feat: Inital commit
This commit is contained in:
+316
@@ -0,0 +1,316 @@
|
||||
// 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"
|
||||
|
||||
pb "cloud.google.com/go/firestore/apiv1/firestorepb"
|
||||
"cloud.google.com/go/internal/fields"
|
||||
"google.golang.org/genproto/googleapis/type/latlng"
|
||||
ts "google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
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))
|
||||
typeOfVector64 = reflect.TypeOf(Vector64{})
|
||||
typeOfVector32 = reflect.TypeOf(Vector32{})
|
||||
)
|
||||
|
||||
// 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{BytesValue: x}}, false, nil
|
||||
case time.Time:
|
||||
return &pb.Value{ValueType: &pb.Value_TimestampValue{TimestampValue: ts.New(x)}}, 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{TimestampValue: x}}, false, nil
|
||||
case Vector32:
|
||||
return vectorToProtoValue(x), false, nil
|
||||
case Vector64:
|
||||
return vectorToProtoValue(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{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{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{BooleanValue: v.Bool()}}, false, nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return &pb.Value{ValueType: &pb.Value_IntegerValue{IntegerValue: v.Int()}}, false, nil
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||
return &pb.Value{ValueType: &pb.Value_IntegerValue{IntegerValue: int64(v.Uint())}}, false, nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatToProtoValue(v.Float()), false, nil
|
||||
case reflect.String:
|
||||
return stringToProtoValue(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())
|
||||
}
|
||||
}
|
||||
|
||||
func stringToProtoValue(s string) *pb.Value {
|
||||
return &pb.Value{ValueType: &pb.Value_StringValue{StringValue: s}}
|
||||
}
|
||||
|
||||
func floatToProtoValue(f float64) *pb.Value {
|
||||
return &pb.Value{ValueType: &pb.Value_DoubleValue{DoubleValue: f}}
|
||||
}
|
||||
|
||||
// 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{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{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{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: %w", 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
|
||||
}
|
||||
Reference in New Issue
Block a user