blob: 7071469aa077406fb4623d10206cbcf053d25ddb [file] [log] [blame]
// Copyright 2015 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package datastore
import (
"fmt"
"reflect"
)
// GetPLS resolves obj into default struct PropertyLoadSaver and
// MetaGetterSetter implementation.
//
// obj must be a non-nil pointer to a struct of some sort.
//
// By default, exported fields will be serialized to/from the datastore. If the
// field is not exported, it will be skipped by the serialization routines.
//
// If a field is of a non-supported type (see Property for the list of supported
// property types), this function will panic. Other problems include duplicate
// field names (due to tagging), recursively defined structs, nested structures
// with multiple slices (e.g. slices of slices, either directly `[][]type` or
// indirectly `[]Embedded` where Embedded contains a slice.)
//
// The following field types are supported:
// * int64, int32, int16, int8, int
// * uint32, uint16, uint8, byte
// * float64, float32
// * string
// * []byte
// * bool
// * time.Time
// * GeoPoint
// * *Key
// * any Type whose underlying type is one of the above types
// * Types which implement PropertyConverter on (*Type)
// * A struct composed of the above types (except for nested slices)
// * A slice of any of the above types
//
// GetPLS supports the following struct tag syntax:
// `gae:"fieldName[,noindex]"` -- an alternate fieldname for an exportable
// field. When the struct is serialized or deserialized, fieldName will be
// associated with the struct field instead of the field's Go name. This is
// useful when writing Go code which interfaces with appengine code written
// in other languages (like python) which use lowercase as their default
// datastore field names.
//
// A fieldName of "-" means that gae will ignore the field for all
// serialization/deserialization.
//
// if noindex is specified, then this field will not be indexed in the
// datastore, even if it was an otherwise indexable type. If fieldName is
// blank, and noindex is specifed, then fieldName will default to the
// field's actual name. Note that by default, all fields (with indexable
// types) are indexed.
//
// `gae:"$metaKey[,<value>]` -- indicates a field is metadata. Metadata
// can be used to control filter behavior, or to store key data when using
// the Interface.KeyForObj* methods. The supported field types are:
// - *Key
// - int64, int32, int16, int8, uint32, uint16, uint8, byte
// - string
// - Toggle (GetMeta and SetMeta treat the field as if it were bool)
// - Any type which implements PropertyConverter
// Additionally, numeric, string and Toggle types allow setting a default
// value in the struct field tag (the "<value>" portion).
//
// Only exported fields allow SetMeta, but all fields of appropriate type
// allow tagged defaults for use with GetMeta. See Examples.
//
// `gae:"[-],extra"` -- indicates that any extra, unrecognized or mismatched
// property types (type in datastore doesn't match your struct's field
// type) should be loaded into and saved from this field. The precise type
// of the field must be PropertyMap. This form allows you to control the
// behavior of reads and writes when your schema changes, or to implement
// something like ndb.Expando with a mix of structured and unstructured
// fields.
//
// If the `-` is present, then datastore write operations will not put
// elements of this map into the datastore.
//
// If the field is non-exported, then read operations from the datastore
// will not populate the members of this map, but extra fields or
// structural differences encountered when reading into this struct will be
// silently ignored. This is useful if you want to just ignore old fields.
//
// If there is a conflict between a field in the struct and a same-named
// Property in the extra field, the field in the struct takes precedence.
//
// Recursive structs are supported, but all extra properties go to the
// topmost structure's Extra field. This is a bit non-intuitive, but the
// implementation complexity was deemed not worth it, since that sort of
// thing is generally only useful on schema changes, which should be
// transient.
//
// Examples:
// // "black hole": ignore mismatches, ignore on write
// _ PropertyMap `gae:"-,extra"
//
// // "expando": full content is read/written
// Expando PropertyMap `gae:",extra"
//
// // "convert": content is read from datastore, but lost on writes. This
// // is useful for doing conversions from an old schema to a new one,
// // since you can retrieve the old data and populate it into new fields,
// // for example. Probably should be used in conjunction with an
// // implementation of the PropertyLoadSaver interface so that you can
// // transparently upconvert to the new schema on load.
// Convert PropertyMap `gae:"-,extra"
//
// Example "special" structure. This is supposed to be some sort of datastore
// singleton object.
// struct secretFoo {
// // _id and _kind are not exported, so setting their values will not be
// // reflected by GetMeta.
// _id int64 `gae:"$id,1"`
// _kind string `gae:"$kind,InternalFooSingleton"`
//
// // Value is exported, so can be read and written by the PropertyLoadSaver,
// // but secretFoo is shared with a python appengine module which has
// // stored this field as 'value' instead of 'Value'.
// Value int64 `gae:"value"`
// }
//
// Example "normal" structure that you might use in a go-only appengine app.
// struct User {
// ID string `gae:"$id"`
// // "kind" is automatically implied by the struct name: "User"
// // "parent" is nil... Users are root entities
//
// // 'Name' will serialized to the datastore in the field 'Name'
// Name string
// }
//
// struct Comment {
// ID int64 `gae:"$id"`
// // "kind" is automatically implied by the struct name: "Comment"
//
// // Parent will be enforced by the application to be a User key.
// Parent *Key `gae:"$parent"`
//
// // 'Lines' will serialized to the datastore in the field 'Lines'
// Lines []string
// }
//
// A pointer-to-struct may also implement MetaGetterSetter to provide more
// sophistocated metadata values. Explicitly defined fields (as shown above)
// always take precedence over fields manipulated by the MetaGetterSetter
// methods. So if your GetMeta handles "kind", but you explicitly have a
// $kind field, the $kind field will take precedence and your GetMeta
// implementation will not be called for "kind".
//
// A struct overloading any of the PropertyLoadSaver or MetaGetterSetter
// interfaces may evoke the default struct behavior by using GetPLS on itself.
// For example:
//
// struct Special {
// Name string
//
// foo string
// }
//
// func (s *Special) Load(props PropertyMap) error {
// if foo, ok := props["foo"]; ok && len(foo) == 1 {
// s.foo = foo
// delete(props, "foo")
// }
// return GetPLS(s).Load(props)
// }
//
// func (s *Special) Save(withMeta bool) (PropertyMap, error) {
// props, err := GetPLS(s).Save(withMeta)
// if err != nil {
// return nil, err
// }
// props["foo"] = []Property{MkProperty(s.foo)}
// return props, nil
// }
//
// func (s *Special) Problem() error {
// return GetPLS(s).Problem()
// }
//
// Additionally, any field ptr-to-type may implement the PropertyConverter
// interface to allow a single field to, for example, implement some alternate
// encoding (json, gzip), or even just serialize to/from a simple string field.
// This applies to normal fields, as well as metadata fields. It can be useful
// for storing struct '$id's which have multi-field meanings. For example, the
// Person struct below could be initialized in go as `&Person{Name{"Jane",
// "Doe"}}`, retaining Jane's name as manipulable Go fields. However, in the
// datastore, it would have a key of `/Person,"Jane|Doe"`, and loading the
// struct from the datastore as part of a Query, for example, would correctly
// populate Person.Name.First and Person.Name.Last.
//
// type Name struct {
// First string
// Last string
// }
//
// func (n *Name) ToProperty() (Property, error) {
// return fmt.Sprintf("%s|%s", n.First, n.Last)
// }
//
// func (n *Name) FromProperty(p Property) error {
// // check p to be a PTString
// // split on "|"
// // assign to n.First, n.Last
// }
//
// type Person struct {
// ID Name `gae:"$id"`
// }
func GetPLS(obj interface{}) interface {
PropertyLoadSaver
MetaGetterSetter
} {
v := reflect.ValueOf(obj)
if !v.IsValid() {
panic(fmt.Errorf("cannot GetPLS(%T): failed to reflect", obj))
}
if v.Kind() == reflect.Ptr {
if v.IsNil() {
panic(fmt.Errorf("cannot GetPLS(%T): pointer is nil", obj))
}
v = v.Elem()
if v.Kind() == reflect.Struct {
s := structPLS{
c: getCodec(v.Type()),
o: v,
}
// If our object implements MetaGetterSetter, use this instead of the built-in
// PLS MetaGetterSetter.
if mgs, ok := obj.(MetaGetterSetter); ok {
s.mgs = mgs
}
return &s
}
}
panic(fmt.Errorf("cannot GetPLS(%T): not a pointer-to-struct", obj))
}
func getMGS(obj interface{}) MetaGetterSetter {
if mgs, ok := obj.(MetaGetterSetter); ok {
return mgs
}
return GetPLS(obj)
}
func getCodec(structType reflect.Type) *structCodec {
structCodecsMutex.RLock()
c, ok := structCodecs[structType]
structCodecsMutex.RUnlock()
if !ok {
structCodecsMutex.Lock()
defer structCodecsMutex.Unlock()
c = getStructCodecLocked(structType)
}
if c.problem != nil {
panic(c.problem)
}
return c
}