| // 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.Array: |
| return arrayToProtoValue(v) |
| 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()) |
| } |
| } |
| |
| // arrayToProtoValue converts a array to a Firestore Value protobuf and reports |
| // whether a transform was encountered. |
| func arrayToProtoValue(v reflect.Value) (*pb.Value, bool, error) { |
| 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 |
| } |
| |
| // 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 |
| } |
| return arrayToProtoValue(v) |
| } |
| |
| // 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 |
| } |