// Copyright 2017 Google LLC // // 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 firestore import ( "errors" "fmt" "reflect" "time" "cloud.google.com/go/internal/fields" "github.com/golang/protobuf/ptypes" ts "github.com/golang/protobuf/ptypes/timestamp" pb "google.golang.org/genproto/googleapis/firestore/v1" "google.golang.org/genproto/googleapis/type/latlng" ) var nullValue = &pb.Value{ValueType: &pb.Value_NullValue{}} var ( typeOfByteSlice = reflect.TypeOf([]byte{}) typeOfGoTime = reflect.TypeOf(time.Time{}) typeOfLatLng = reflect.TypeOf((*latlng.LatLng)(nil)) typeOfDocumentRef = reflect.TypeOf((*DocumentRef)(nil)) typeOfProtoTimestamp = reflect.TypeOf((*ts.Timestamp)(nil)) ) // toProtoValue converts a Go value to a Firestore Value protobuf. // Some corner cases: // - All nils (nil interface, nil slice, nil map, nil pointer) are converted to // a NullValue (not a nil *pb.Value). toProtoValue never returns (nil, false, nil). // It returns (nil, true, nil) if everything in the value is ServerTimestamp. // - An error is returned for uintptr, uint and uint64, because Firestore uses // an int64 to represent integral values, and those types can't be properly // represented in an int64. // - An error is returned for the special Delete value. // // toProtoValue also reports whether it recursively encountered a transform. func toProtoValue(v reflect.Value) (pbv *pb.Value, sawTransform bool, err error) { if !v.IsValid() { return nullValue, false, nil } vi := v.Interface() if vi == Delete { return nil, false, errors.New("firestore: cannot use Delete in value") } if vi == ServerTimestamp { return nil, false, errors.New("firestore: must use ServerTimestamp as a map value") } switch x := vi.(type) { case []byte: return &pb.Value{ValueType: &pb.Value_BytesValue{x}}, false, nil case time.Time: ts, err := ptypes.TimestampProto(x) if err != nil { return nil, false, err } return &pb.Value{ValueType: &pb.Value_TimestampValue{ts}}, false, nil case *ts.Timestamp: if x == nil { // gRPC doesn't like nil oneofs. Use NullValue. return nullValue, false, nil } return &pb.Value{ValueType: &pb.Value_TimestampValue{x}}, false, nil case *latlng.LatLng: if x == nil { // gRPC doesn't like nil oneofs. Use NullValue. return nullValue, false, nil } return &pb.Value{ValueType: &pb.Value_GeoPointValue{x}}, false, nil case *DocumentRef: if x == nil { // gRPC doesn't like nil oneofs. Use NullValue. return nullValue, false, nil } return &pb.Value{ValueType: &pb.Value_ReferenceValue{x.Path}}, false, nil // Do not add bool, string, int, etc. to this switch; leave them in the // reflect-based switch below. Moving them here would drop support for // types whose underlying types are those primitives. // E.g. Given "type mybool bool", an ordinary type switch on bool will // not catch a mybool, but the reflect.Kind of a mybool is reflect.Bool. } switch v.Kind() { case reflect.Bool: return &pb.Value{ValueType: &pb.Value_BooleanValue{v.Bool()}}, false, nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return &pb.Value{ValueType: &pb.Value_IntegerValue{v.Int()}}, false, nil case reflect.Uint8, reflect.Uint16, reflect.Uint32: return &pb.Value{ValueType: &pb.Value_IntegerValue{int64(v.Uint())}}, false, nil case reflect.Float32, reflect.Float64: return &pb.Value{ValueType: &pb.Value_DoubleValue{v.Float()}}, false, nil case reflect.String: return &pb.Value{ValueType: &pb.Value_StringValue{v.String()}}, false, nil case reflect.Slice: return sliceToProtoValue(v) case reflect.Map: return mapToProtoValue(v) case reflect.Struct: return structToProtoValue(v) case reflect.Ptr: if v.IsNil() { return nullValue, false, nil } return toProtoValue(v.Elem()) case reflect.Interface: if v.NumMethod() == 0 { // empty interface: recurse on its contents return toProtoValue(v.Elem()) } fallthrough // any other interface value is an error default: return nil, false, fmt.Errorf("firestore: cannot convert type %s to value", v.Type()) } } // sliceToProtoValue converts a slice to a Firestore Value protobuf and reports // whether a transform was encountered. func sliceToProtoValue(v reflect.Value) (*pb.Value, bool, error) { // A nil slice is converted to a null value. if v.IsNil() { return nullValue, false, nil } vals := make([]*pb.Value, v.Len()) for i := 0; i < v.Len(); i++ { val, sawTransform, err := toProtoValue(v.Index(i)) if err != nil { return nil, false, err } if sawTransform { return nil, false, fmt.Errorf("firestore: transforms cannot occur in an array, but saw some in %v", v.Index(i)) } vals[i] = val } return &pb.Value{ValueType: &pb.Value_ArrayValue{&pb.ArrayValue{Values: vals}}}, false, nil } // mapToProtoValue converts a map to a Firestore Value protobuf and reports whether // a transform was encountered. func mapToProtoValue(v reflect.Value) (*pb.Value, bool, error) { if v.Type().Key().Kind() != reflect.String { return nil, false, errors.New("firestore: map key type must be string") } // A nil map is converted to a null value. if v.IsNil() { return nullValue, false, nil } m := map[string]*pb.Value{} sawTransform := false for _, k := range v.MapKeys() { mi := v.MapIndex(k) if mi.Interface() == ServerTimestamp { sawTransform = true continue } else if _, ok := mi.Interface().(arrayUnion); ok { sawTransform = true continue } else if _, ok := mi.Interface().(arrayRemove); ok { sawTransform = true continue } val, sst, err := toProtoValue(mi) if err != nil { return nil, false, err } if sst { sawTransform = true } if val == nil { // value was a map with all ServerTimestamp values continue } m[k.String()] = val } var pv *pb.Value if len(m) == 0 && sawTransform { // The entire map consisted of transform values. pv = nil } else { pv = &pb.Value{ValueType: &pb.Value_MapValue{&pb.MapValue{Fields: m}}} } return pv, sawTransform, nil } // structToProtoValue converts a struct to a Firestore Value protobuf and reports // whether a transform was encountered. func structToProtoValue(v reflect.Value) (*pb.Value, bool, error) { m := map[string]*pb.Value{} fields, err := fieldCache.Fields(v.Type()) if err != nil { return nil, false, err } sawTransform := false if _, ok := v.Interface().(arrayUnion); ok { return nil, false, errors.New("firestore: ArrayUnion may not be used in structs") } if _, ok := v.Interface().(arrayRemove); ok { return nil, false, errors.New("firestore: ArrayRemove may not be used in structs") } for _, f := range fields { fv := v.FieldByIndex(f.Index) opts := f.ParsedTag.(tagOptions) if opts.serverTimestamp { // TODO(jba): should we return a non-zero time? sawTransform = true continue } if opts.omitEmpty && isEmptyValue(fv) { continue } val, sst, err := toProtoValue(fv) if err != nil { return nil, false, err } if sst { sawTransform = true } if val == nil { // value was a map with all ServerTimestamp values continue } m[f.Name] = val } var pv *pb.Value if len(m) == 0 && sawTransform { // The entire struct consisted of ServerTimestamp or omitempty values. pv = nil } else { pv = &pb.Value{ValueType: &pb.Value_MapValue{&pb.MapValue{Fields: m}}} } return pv, sawTransform, nil } type tagOptions struct { omitEmpty bool // do not marshal value if empty serverTimestamp bool // set time.Time to server timestamp on write } // parseTag interprets firestore struct field tags. func parseTag(t reflect.StructTag) (name string, keep bool, other interface{}, err error) { name, keep, opts, err := fields.ParseStandardTag("firestore", t) if err != nil { return "", false, nil, fmt.Errorf("firestore: %v", err) } tagOpts := tagOptions{} for _, opt := range opts { switch opt { case "omitempty": tagOpts.omitEmpty = true case "serverTimestamp": tagOpts.serverTimestamp = true default: return "", false, nil, fmt.Errorf("firestore: unknown tag option: %q", opt) } } return name, keep, tagOpts, nil } // isLeafType determines whether or not a type is a 'leaf type' // and should not be recursed into, but considered one field. func isLeafType(t reflect.Type) bool { return t == typeOfGoTime || t == typeOfLatLng || t == typeOfProtoTimestamp } var fieldCache = fields.NewCache(parseTag, nil, isLeafType) // isEmptyValue is taken from the encoding/json package in the // standard library. // TODO(jba): move to the fields package func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() } if v.Type() == typeOfGoTime { return v.Interface().(time.Time).IsZero() } return false }