diff options
Diffstat (limited to 'src/mongo/gotools/common/bsonutil')
-rw-r--r-- | src/mongo/gotools/common/bsonutil/bsonutil.go | 416 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/converter.go | 388 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/converter_test.go | 345 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/date_test.go | 169 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/marshal_d.go | 59 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/marshal_d_test.go | 124 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/maxkey_test.go | 38 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/minkey_test.go | 38 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/number.go | 18 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/numberint_test.go | 37 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/numberlong_test.go | 37 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/objectid_test.go | 38 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/regexp_test.go | 66 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/timestamp_test.go | 43 | ||||
-rw-r--r-- | src/mongo/gotools/common/bsonutil/undefined_test.go | 38 |
15 files changed, 1854 insertions, 0 deletions
diff --git a/src/mongo/gotools/common/bsonutil/bsonutil.go b/src/mongo/gotools/common/bsonutil/bsonutil.go new file mode 100644 index 00000000000..2bda211d547 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/bsonutil.go @@ -0,0 +1,416 @@ +// Package bsonutil provides utilities for processing BSON data. +package bsonutil + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "github.com/mongodb/mongo-tools/common/json" + "github.com/mongodb/mongo-tools/common/util" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + "strconv" + "time" +) + +var ErrNoSuchField = errors.New("no such field") + +// ConvertJSONDocumentToBSON iterates through the document map and converts JSON +// values to their corresponding BSON values. It also replaces any extended JSON +// type value (e.g. $date) with the corresponding BSON type. +func ConvertJSONDocumentToBSON(doc map[string]interface{}) error { + for key, jsonValue := range doc { + var bsonValue interface{} + var err error + + switch v := jsonValue.(type) { + case map[string]interface{}, bson.D: // subdocument + bsonValue, err = ParseSpecialKeys(v) + default: + bsonValue, err = ConvertJSONValueToBSON(v) + } + if err != nil { + return err + } + + doc[key] = bsonValue + } + return nil +} + +// GetExtendedBsonD iterates through the document and returns a bson.D that adds type +// information for each key in document. +func GetExtendedBsonD(doc bson.D) (bson.D, error) { + var err error + var bsonDoc bson.D + for _, docElem := range doc { + var bsonValue interface{} + switch v := docElem.Value.(type) { + case map[string]interface{}, bson.D: // subdocument + bsonValue, err = ParseSpecialKeys(v) + default: + bsonValue, err = ConvertJSONValueToBSON(v) + } + if err != nil { + return nil, err + } + bsonDoc = append(bsonDoc, bson.DocElem{ + Name: docElem.Name, + Value: bsonValue, + }) + } + return bsonDoc, nil +} + +// FindValueByKey returns the value of keyName in document. If keyName is not found +// in the top-level of the document, ErrNoSuchField is returned as the error. +func FindValueByKey(keyName string, document *bson.D) (interface{}, error) { + for _, key := range *document { + if key.Name == keyName { + return key.Value, nil + } + } + return nil, ErrNoSuchField +} + +// ParseSpecialKeys takes a JSON document and inspects it for any extended JSON +// type (e.g $numberLong) and replaces any such values with the corresponding +// BSON type. +func ParseSpecialKeys(special interface{}) (interface{}, error) { + // first ensure we are using a correct document type + var doc map[string]interface{} + switch v := special.(type) { + case bson.D: + doc = v.Map() + case map[string]interface{}: + doc = v + default: + return nil, fmt.Errorf("%v (type %T) is not valid input to ParseSpecialKeys", special, special) + } + // check document to see if it is special + switch len(doc) { + case 1: // document has a single field + if jsonValue, ok := doc["$date"]; ok { + switch v := jsonValue.(type) { + case string: + return util.FormatDate(v) + case bson.D: + asMap := v.Map() + if jsonValue, ok := asMap["$numberLong"]; ok { + n, err := parseNumberLongField(jsonValue) + if err != nil { + return nil, err + } + return time.Unix(n/1e3, n%1e3*1e6), err + } + return nil, errors.New("expected $numberLong field in $date") + case map[string]interface{}: + if jsonValue, ok := v["$numberLong"]; ok { + n, err := parseNumberLongField(jsonValue) + if err != nil { + return nil, err + } + return time.Unix(n/1e3, n%1e3*1e6), err + } + return nil, errors.New("expected $numberLong field in $date") + + case json.Number: + n, err := v.Int64() + return time.Unix(n/1e3, n%1e3*1e6), err + case float64: + n := int64(v) + return time.Unix(n/1e3, n%1e3*1e6), nil + case int32: + n := int64(v) + return time.Unix(n/1e3, n%1e3*1e6), nil + case int64: + return time.Unix(v/1e3, v%1e3*1e6), nil + + case json.ISODate: + return v, nil + + default: + return nil, errors.New("invalid type for $date field") + } + } + + if jsonValue, ok := doc["$code"]; ok { + switch v := jsonValue.(type) { + case string: + return bson.JavaScript{Code: v}, nil + default: + return nil, errors.New("expected $code field to have string value") + } + } + + if jsonValue, ok := doc["$oid"]; ok { + switch v := jsonValue.(type) { + case string: + if !bson.IsObjectIdHex(v) { + return nil, errors.New("expected $oid field to contain 24 hexadecimal character") + } + return bson.ObjectIdHex(v), nil + + default: + return nil, errors.New("expected $oid field to have string value") + } + } + + if jsonValue, ok := doc["$numberLong"]; ok { + return parseNumberLongField(jsonValue) + } + + if jsonValue, ok := doc["$numberInt"]; ok { + switch v := jsonValue.(type) { + case string: + // all of decimal, hex, and octal are supported here + n, err := strconv.ParseInt(v, 0, 32) + return int32(n), err + + default: + return nil, errors.New("expected $numberInt field to have string value") + } + } + + if jsonValue, ok := doc["$timestamp"]; ok { + ts := json.Timestamp{} + + var tsDoc map[string]interface{} + switch internalDoc := jsonValue.(type) { + case map[string]interface{}: + tsDoc = internalDoc + case bson.D: + tsDoc = internalDoc.Map() + default: + return nil, errors.New("expected $timestamp key to have internal document") + } + + if seconds, ok := tsDoc["t"]; ok { + if asUint32, err := util.ToUInt32(seconds); err == nil { + ts.Seconds = asUint32 + } else { + return nil, errors.New("expected $timestamp 't' field to be a numeric type") + } + } else { + return nil, errors.New("expected $timestamp to have 't' field") + } + if inc, ok := tsDoc["i"]; ok { + if asUint32, err := util.ToUInt32(inc); err == nil { + ts.Increment = asUint32 + } else { + return nil, errors.New("expected $timestamp 'i' field to be a numeric type") + } + } else { + return nil, errors.New("expected $timestamp to have 'i' field") + } + // see BSON spec for details on the bit fiddling here + return bson.MongoTimestamp(int64(ts.Seconds)<<32 | int64(ts.Increment)), nil + } + + if jsonValue, ok := doc["$numberDecimal"]; ok { + switch v := jsonValue.(type) { + case string: + return bson.ParseDecimal128(v) + default: + return nil, errors.New("expected $numberDecimal field to have string value") + } + } + + if _, ok := doc["$undefined"]; ok { + return bson.Undefined, nil + } + + if _, ok := doc["$maxKey"]; ok { + return bson.MaxKey, nil + } + + if _, ok := doc["$minKey"]; ok { + return bson.MinKey, nil + } + + case 2: // document has two fields + if jsonValue, ok := doc["$code"]; ok { + code := bson.JavaScript{} + switch v := jsonValue.(type) { + case string: + code.Code = v + default: + return nil, errors.New("expected $code field to have string value") + } + + if jsonValue, ok = doc["$scope"]; ok { + switch v2 := jsonValue.(type) { + case map[string]interface{}, bson.D: + x, err := ParseSpecialKeys(v2) + if err != nil { + return nil, err + } + code.Scope = x + return code, nil + default: + return nil, errors.New("expected $scope field to contain map") + } + } else { + return nil, errors.New("expected $scope field with $code field") + } + } + + if jsonValue, ok := doc["$regex"]; ok { + regex := bson.RegEx{} + + switch pattern := jsonValue.(type) { + case string: + regex.Pattern = pattern + + default: + return nil, errors.New("expected $regex field to have string value") + } + if jsonValue, ok = doc["$options"]; !ok { + return nil, errors.New("expected $options field with $regex field") + } + + switch options := jsonValue.(type) { + case string: + regex.Options = options + + default: + return nil, errors.New("expected $options field to have string value") + } + + // Validate regular expression options + for i := range regex.Options { + switch o := regex.Options[i]; o { + default: + return nil, fmt.Errorf("invalid regular expression option '%v'", o) + + case 'g', 'i', 'm', 's': // allowed + } + } + return regex, nil + } + + if jsonValue, ok := doc["$binary"]; ok { + binary := bson.Binary{} + + switch data := jsonValue.(type) { + case string: + bytes, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return nil, err + } + binary.Data = bytes + + default: + return nil, errors.New("expected $binary field to have string value") + } + if jsonValue, ok = doc["$type"]; !ok { + return nil, errors.New("expected $type field with $binary field") + } + + switch typ := jsonValue.(type) { + case string: + kind, err := hex.DecodeString(typ) + if err != nil { + return nil, err + } else if len(kind) != 1 { + return nil, errors.New("expected single byte (as hexadecimal string) for $type field") + } + binary.Kind = kind[0] + + default: + return nil, errors.New("expected $type field to have string value") + } + return binary, nil + } + + if jsonValue, ok := doc["$ref"]; ok { + dbRef := mgo.DBRef{} + + switch data := jsonValue.(type) { + case string: + dbRef.Collection = data + default: + return nil, errors.New("expected string for $ref field") + } + if jsonValue, ok = doc["$id"]; ok { + switch v2 := jsonValue.(type) { + case map[string]interface{}, bson.D: + x, err := ParseSpecialKeys(v2) + if err != nil { + return nil, fmt.Errorf("error parsing $id field: %v", err) + } + dbRef.Id = x + default: + dbRef.Id = v2 + } + return dbRef, nil + } + } + case 3: + if jsonValue, ok := doc["$ref"]; ok { + dbRef := mgo.DBRef{} + + switch data := jsonValue.(type) { + case string: + dbRef.Collection = data + default: + return nil, errors.New("expected string for $ref field") + } + if jsonValue, ok = doc["$id"]; ok { + switch v2 := jsonValue.(type) { + case map[string]interface{}, bson.D: + x, err := ParseSpecialKeys(v2) + if err != nil { + return nil, fmt.Errorf("error parsing $id field: %v", err) + } + dbRef.Id = x + default: + dbRef.Id = v2 + } + if dbValue, ok := doc["$db"]; ok { + switch v3 := dbValue.(type) { + case string: + dbRef.Database = v3 + default: + return nil, errors.New("expected string for $db field") + } + return dbRef, nil + } + } + } + } + + // nothing matched, so we recurse deeper + switch v := special.(type) { + case bson.D: + return GetExtendedBsonD(v) + case map[string]interface{}: + return ConvertJSONValueToBSON(v) + default: + return nil, fmt.Errorf("%v (type %T) is not valid input to ParseSpecialKeys", special, special) + } +} + +// ParseJSONValue takes any value generated by the json package and returns a +// BSON version of that value. +func ParseJSONValue(jsonValue interface{}) (interface{}, error) { + switch v := jsonValue.(type) { + case map[string]interface{}, bson.D: // subdocument + return ParseSpecialKeys(v) + + default: + return ConvertJSONValueToBSON(v) + } +} + +func parseNumberLongField(jsonValue interface{}) (int64, error) { + switch v := jsonValue.(type) { + case string: + // all of decimal, hex, and octal are supported here + return strconv.ParseInt(v, 0, 64) + + default: + return 0, errors.New("expected $numberLong field to have string value") + } +} diff --git a/src/mongo/gotools/common/bsonutil/converter.go b/src/mongo/gotools/common/bsonutil/converter.go new file mode 100644 index 00000000000..02d091e21a4 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/converter.go @@ -0,0 +1,388 @@ +package bsonutil + +import ( + "encoding/base64" + "errors" + "fmt" + "github.com/mongodb/mongo-tools/common/json" + "github.com/mongodb/mongo-tools/common/util" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + "time" +) + +// ConvertJSONValueToBSON walks through a document or an array and +// replaces any extended JSON value with its corresponding BSON type. +func ConvertJSONValueToBSON(x interface{}) (interface{}, error) { + switch v := x.(type) { + case nil: + return nil, nil + case bool: + return v, nil + case map[string]interface{}: // document + for key, jsonValue := range v { + bsonValue, err := ParseJSONValue(jsonValue) + if err != nil { + return nil, err + } + v[key] = bsonValue + } + return v, nil + case bson.D: + for i := range v { + var err error + v[i].Value, err = ParseJSONValue(v[i].Value) + if err != nil { + return nil, err + } + } + return v, nil + + case []interface{}: // array + for i, jsonValue := range v { + bsonValue, err := ParseJSONValue(jsonValue) + if err != nil { + return nil, err + } + v[i] = bsonValue + } + return v, nil + + case string, float64, int32, int64: + return v, nil // require no conversion + + case json.ObjectId: // ObjectId + s := string(v) + if !bson.IsObjectIdHex(s) { + return nil, errors.New("expected ObjectId to contain 24 hexadecimal characters") + } + return bson.ObjectIdHex(s), nil + + case json.Decimal128: + return v.Decimal128, nil + + case json.Date: // Date + n := int64(v) + return time.Unix(n/1e3, n%1e3*1e6), nil + + case json.ISODate: // ISODate + n := string(v) + return util.FormatDate(n) + + case json.NumberLong: // NumberLong + return int64(v), nil + + case json.NumberInt: // NumberInt + return int32(v), nil + + case json.NumberFloat: // NumberFloat + return float64(v), nil + case json.BinData: // BinData + data, err := base64.StdEncoding.DecodeString(v.Base64) + if err != nil { + return nil, err + } + return bson.Binary{v.Type, data}, nil + + case json.DBRef: // DBRef + var err error + v.Id, err = ParseJSONValue(v.Id) + if err != nil { + return nil, err + } + return mgo.DBRef{v.Collection, v.Id, v.Database}, nil + + case json.DBPointer: // DBPointer, for backwards compatibility + return bson.DBPointer{v.Namespace, v.Id}, nil + + case json.RegExp: // RegExp + return bson.RegEx{v.Pattern, v.Options}, nil + + case json.Timestamp: // Timestamp + ts := (int64(v.Seconds) << 32) | int64(v.Increment) + return bson.MongoTimestamp(ts), nil + + case json.JavaScript: // Javascript + return bson.JavaScript{v.Code, v.Scope}, nil + + case json.MinKey: // MinKey + return bson.MinKey, nil + + case json.MaxKey: // MaxKey + return bson.MaxKey, nil + + case json.Undefined: // undefined + return bson.Undefined, nil + + default: + return nil, fmt.Errorf("conversion of JSON value '%v' of type '%T' not supported", v, v) + } +} + +func convertKeys(v bson.M) (bson.M, error) { + for key, value := range v { + jsonValue, err := ConvertBSONValueToJSON(value) + if err != nil { + return nil, err + } + v[key] = jsonValue + } + return v, nil +} + +func getConvertedKeys(v bson.M) (bson.M, error) { + out := bson.M{} + for key, value := range v { + jsonValue, err := GetBSONValueAsJSON(value) + if err != nil { + return nil, err + } + out[key] = jsonValue + } + return out, nil +} + +// ConvertBSONValueToJSON walks through a document or an array and +// converts any BSON value to its corresponding extended JSON type. +// It returns the converted JSON document and any error encountered. +func ConvertBSONValueToJSON(x interface{}) (interface{}, error) { + switch v := x.(type) { + case nil: + return nil, nil + case bool: + return v, nil + + case *bson.M: // document + doc, err := convertKeys(*v) + if err != nil { + return nil, err + } + return doc, err + case bson.M: // document + return convertKeys(v) + case map[string]interface{}: + return convertKeys(v) + case bson.D: + for i, value := range v { + jsonValue, err := ConvertBSONValueToJSON(value.Value) + if err != nil { + return nil, err + } + v[i].Value = jsonValue + } + return MarshalD(v), nil + case MarshalD: + return v, nil + case []interface{}: // array + for i, value := range v { + jsonValue, err := ConvertBSONValueToJSON(value) + if err != nil { + return nil, err + } + v[i] = jsonValue + } + return v, nil + + case string: + return v, nil // require no conversion + + case int: + return json.NumberInt(v), nil + + case bson.ObjectId: // ObjectId + return json.ObjectId(v.Hex()), nil + + case bson.Decimal128: + return json.Decimal128{v}, nil + + case time.Time: // Date + return json.Date(v.Unix()*1000 + int64(v.Nanosecond()/1e6)), nil + + case int64: // NumberLong + return json.NumberLong(v), nil + + case int32: // NumberInt + return json.NumberInt(v), nil + + case float64: + return json.NumberFloat(v), nil + + case float32: + return json.NumberFloat(float64(v)), nil + + case []byte: // BinData (with generic type) + data := base64.StdEncoding.EncodeToString(v) + return json.BinData{0x00, data}, nil + + case bson.Binary: // BinData + data := base64.StdEncoding.EncodeToString(v.Data) + return json.BinData{v.Kind, data}, nil + + case mgo.DBRef: // DBRef + return json.DBRef{v.Collection, v.Id, v.Database}, nil + + case bson.DBPointer: // DBPointer + return json.DBPointer{v.Namespace, v.Id}, nil + + case bson.RegEx: // RegExp + return json.RegExp{v.Pattern, v.Options}, nil + + case bson.MongoTimestamp: // Timestamp + timestamp := int64(v) + return json.Timestamp{ + Seconds: uint32(timestamp >> 32), + Increment: uint32(timestamp), + }, nil + + case bson.JavaScript: // JavaScript + var scope interface{} + var err error + if v.Scope != nil { + scope, err = ConvertBSONValueToJSON(v.Scope) + if err != nil { + return nil, err + } + } + return json.JavaScript{v.Code, scope}, nil + + default: + switch x { + case bson.MinKey: // MinKey + return json.MinKey{}, nil + + case bson.MaxKey: // MaxKey + return json.MaxKey{}, nil + + case bson.Undefined: // undefined + return json.Undefined{}, nil + } + } + + return nil, fmt.Errorf("conversion of BSON value '%v' of type '%T' not supported", x, x) +} + +// GetBSONValueAsJSON is equivalent to ConvertBSONValueToJSON, but does not mutate its argument. +func GetBSONValueAsJSON(x interface{}) (interface{}, error) { + switch v := x.(type) { + case nil: + return nil, nil + case bool: + return v, nil + + case *bson.M: // document + doc, err := getConvertedKeys(*v) + if err != nil { + return nil, err + } + return doc, err + case bson.M: // document + return getConvertedKeys(v) + case map[string]interface{}: + return getConvertedKeys(v) + case bson.D: + out := bson.D{} + for _, value := range v { + jsonValue, err := GetBSONValueAsJSON(value.Value) + if err != nil { + return nil, err + } + out = append(out, bson.DocElem{ + Name: value.Name, + Value: jsonValue, + }) + } + return MarshalD(out), nil + case MarshalD: + out, err := GetBSONValueAsJSON(bson.D(v)) + if err != nil { + return nil, err + } + return MarshalD(out.(bson.D)), nil + case []interface{}: // array + out := []interface{}{} + for _, value := range v { + jsonValue, err := GetBSONValueAsJSON(value) + if err != nil { + return nil, err + } + out = append(out, jsonValue) + } + return out, nil + + case string: + return v, nil // require no conversion + + case int: + return json.NumberInt(v), nil + + case bson.ObjectId: // ObjectId + return json.ObjectId(v.Hex()), nil + + case bson.Decimal128: + return json.Decimal128{v}, nil + + case time.Time: // Date + return json.Date(v.Unix()*1000 + int64(v.Nanosecond()/1e6)), nil + + case int64: // NumberLong + return json.NumberLong(v), nil + + case int32: // NumberInt + return json.NumberInt(v), nil + + case float64: + return json.NumberFloat(v), nil + + case float32: + return json.NumberFloat(float64(v)), nil + + case []byte: // BinData (with generic type) + data := base64.StdEncoding.EncodeToString(v) + return json.BinData{0x00, data}, nil + + case bson.Binary: // BinData + data := base64.StdEncoding.EncodeToString(v.Data) + return json.BinData{v.Kind, data}, nil + + case mgo.DBRef: // DBRef + return json.DBRef{v.Collection, v.Id, v.Database}, nil + + case bson.DBPointer: // DBPointer + return json.DBPointer{v.Namespace, v.Id}, nil + + case bson.RegEx: // RegExp + return json.RegExp{v.Pattern, v.Options}, nil + + case bson.MongoTimestamp: // Timestamp + timestamp := int64(v) + return json.Timestamp{ + Seconds: uint32(timestamp >> 32), + Increment: uint32(timestamp), + }, nil + + case bson.JavaScript: // JavaScript + var scope interface{} + var err error + if v.Scope != nil { + scope, err = GetBSONValueAsJSON(v.Scope) + if err != nil { + return nil, err + } + } + return json.JavaScript{v.Code, scope}, nil + + default: + switch x { + case bson.MinKey: // MinKey + return json.MinKey{}, nil + + case bson.MaxKey: // MaxKey + return json.MaxKey{}, nil + + case bson.Undefined: // undefined + return json.Undefined{}, nil + } + } + + return nil, fmt.Errorf("conversion of BSON value '%v' of type '%T' not supported", x, x) +} diff --git a/src/mongo/gotools/common/bsonutil/converter_test.go b/src/mongo/gotools/common/bsonutil/converter_test.go new file mode 100644 index 00000000000..8d329057f52 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/converter_test.go @@ -0,0 +1,345 @@ +package bsonutil + +import ( + "encoding/base64" + "fmt" + "github.com/mongodb/mongo-tools/common/json" + "github.com/mongodb/mongo-tools/common/testutil" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" + "testing" + "time" +) + +func TestObjectIdBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON ObjectId", t, func() { + Convey("that is valid to JSON should produce a json.ObjectId", func() { + bsonObjId := bson.NewObjectId() + jsonObjId := json.ObjectId(bsonObjId.Hex()) + + _jObjId, err := ConvertBSONValueToJSON(bsonObjId) + So(err, ShouldBeNil) + jObjId, ok := _jObjId.(json.ObjectId) + So(ok, ShouldBeTrue) + + So(jObjId, ShouldNotEqual, bsonObjId) + So(jObjId, ShouldEqual, jsonObjId) + }) + }) +} + +func TestArraysBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting BSON arrays to JSON arrays", t, func() { + Convey("should work for empty arrays", func() { + jArr, err := ConvertBSONValueToJSON([]interface{}{}) + So(err, ShouldBeNil) + + So(jArr, ShouldResemble, []interface{}{}) + }) + + Convey("should work for one-level deep arrays", func() { + objId := bson.NewObjectId() + bsonArr := []interface{}{objId, 28, 0.999, "plain"} + _jArr, err := ConvertBSONValueToJSON(bsonArr) + So(err, ShouldBeNil) + jArr, ok := _jArr.([]interface{}) + So(ok, ShouldBeTrue) + + So(len(jArr), ShouldEqual, 4) + So(jArr[0], ShouldEqual, json.ObjectId(objId.Hex())) + So(jArr[1], ShouldEqual, 28) + So(jArr[2], ShouldEqual, 0.999) + So(jArr[3], ShouldEqual, "plain") + }) + + Convey("should work for arrays with embedded objects", func() { + bsonObj := []interface{}{ + 80, + bson.M{ + "a": int64(20), + "b": bson.M{ + "c": bson.RegEx{Pattern: "hi", Options: "i"}, + }, + }, + } + + __jObj, err := ConvertBSONValueToJSON(bsonObj) + So(err, ShouldBeNil) + _jObj, ok := __jObj.([]interface{}) + So(ok, ShouldBeTrue) + jObj, ok := _jObj[1].(bson.M) + So(ok, ShouldBeTrue) + So(len(jObj), ShouldEqual, 2) + So(jObj["a"], ShouldEqual, json.NumberLong(20)) + jjObj, ok := jObj["b"].(bson.M) + So(ok, ShouldBeTrue) + + So(jjObj["c"], ShouldResemble, json.RegExp{"hi", "i"}) + So(jjObj["c"], ShouldNotResemble, json.RegExp{"i", "hi"}) + }) + + }) +} + +func TestDateBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + timeNow := time.Now() + secs := int64(timeNow.Unix()) + nanosecs := timeNow.Nanosecond() + millis := int64(nanosecs / 1e6) + + timeNowSecs := time.Unix(secs, int64(0)) + timeNowMillis := time.Unix(secs, int64(millis*1e6)) + + Convey("Converting BSON time.Time 's dates to JSON", t, func() { + // json.Date is stored as an int64 representing the number of milliseconds since the epoch + Convey(fmt.Sprintf("should work with second granularity: %v", timeNowSecs), func() { + _jObj, err := ConvertBSONValueToJSON(timeNowSecs) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.Date) + So(ok, ShouldBeTrue) + + So(int64(jObj), ShouldEqual, secs*1e3) + }) + + Convey(fmt.Sprintf("should work with millisecond granularity: %v", timeNowMillis), func() { + _jObj, err := ConvertBSONValueToJSON(timeNowMillis) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.Date) + So(ok, ShouldBeTrue) + + So(int64(jObj), ShouldEqual, secs*1e3+millis) + }) + + Convey(fmt.Sprintf("should work with nanosecond granularity: %v", timeNow), func() { + _jObj, err := ConvertBSONValueToJSON(timeNow) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.Date) + So(ok, ShouldBeTrue) + + // we lose nanosecond precision + So(int64(jObj), ShouldEqual, secs*1e3+millis) + }) + + }) +} + +func TestMaxKeyBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON Maxkey to JSON", t, func() { + Convey("should produce a json.MaxKey", func() { + _jObj, err := ConvertBSONValueToJSON(bson.MaxKey) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.MaxKey) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.MaxKey{}) + }) + }) +} + +func TestMinKeyBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON Maxkey to JSON", t, func() { + Convey("should produce a json.MinKey", func() { + _jObj, err := ConvertBSONValueToJSON(bson.MinKey) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.MinKey) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.MinKey{}) + }) + }) +} + +func Test64BitIntBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON int64 to JSON", t, func() { + Convey("should produce a json.NumberLong", func() { + _jObj, err := ConvertBSONValueToJSON(int32(243)) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.NumberInt) + So(ok, ShouldBeTrue) + + So(jObj, ShouldEqual, json.NumberInt(243)) + }) + }) + +} + +func Test32BitIntBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON int32 integer to JSON", t, func() { + Convey("should produce a json.NumberInt", func() { + _jObj, err := ConvertBSONValueToJSON(int64(888234334343)) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.NumberLong) + So(ok, ShouldBeTrue) + + So(jObj, ShouldEqual, json.NumberLong(888234334343)) + }) + }) + +} + +func TestRegExBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON Regular Expression (= /decision/gi) to JSON", t, func() { + Convey("should produce a json.RegExp", func() { + _jObj, err := ConvertBSONValueToJSON(bson.RegEx{"decision", "gi"}) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.RegExp) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.RegExp{"decision", "gi"}) + }) + }) + +} + +func TestUndefinedValueBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON Undefined type to JSON", t, func() { + Convey("should produce a json.Undefined", func() { + _jObj, err := ConvertBSONValueToJSON(bson.Undefined) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.Undefined) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.Undefined{}) + }) + }) +} + +func TestDBRefBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting BSON DBRef to JSON", t, func() { + Convey("should produce a json.DBRef", func() { + _jObj, err := ConvertBSONValueToJSON(mgo.DBRef{"coll1", "some_id", "test"}) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.DBRef) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.DBRef{"coll1", "some_id", "test"}) + So(jObj, ShouldNotResemble, json.DBRef{"coll1", "test", "some_id"}) + }) + }) +} + +func TestTimestampBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting a BSON Timestamp to JSON", t, func() { + Convey("should produce a json.Timestamp", func() { + // {t:803434343, i:9} == bson.MongoTimestamp(803434343*2**32 + 9) + _jObj, err := ConvertBSONValueToJSON(bson.MongoTimestamp(uint64(803434343<<32) | uint64(9))) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.Timestamp) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.Timestamp{Seconds: 803434343, Increment: 9}) + So(jObj, ShouldNotResemble, json.Timestamp{Seconds: 803434343, Increment: 8}) + }) + }) +} + +func TestBinaryBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting BSON Binary data to JSON", t, func() { + Convey("should produce a json.BinData", func() { + _jObj, err := ConvertBSONValueToJSON(bson.Binary{'\x01', []byte("\x05\x20\x02\xae\xf7")}) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.BinData) + So(ok, ShouldBeTrue) + + base64data1 := base64.StdEncoding.EncodeToString([]byte("\x05\x20\x02\xae\xf7")) + base64data2 := base64.StdEncoding.EncodeToString([]byte("\x05\x20\x02\xaf\xf7")) + So(jObj, ShouldResemble, json.BinData{'\x01', base64data1}) + So(jObj, ShouldNotResemble, json.BinData{'\x01', base64data2}) + }) + }) +} + +func TestGenericBytesBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting Go bytes to JSON", t, func() { + Convey("should produce a json.BinData with Type=0x00 (Generic)", func() { + _jObj, err := ConvertBSONValueToJSON([]byte("this is something that's cool")) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.BinData) + So(ok, ShouldBeTrue) + + base64data := base64.StdEncoding.EncodeToString([]byte("this is something that's cool")) + So(jObj, ShouldResemble, json.BinData{0x00, base64data}) + So(jObj, ShouldNotResemble, json.BinData{0x01, base64data}) + }) + }) +} + +func TestUnknownBSONTypeToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting an unknown BSON type to JSON", t, func() { + Convey("should produce an error", func() { + _, err := ConvertBSONValueToJSON(func() {}) + So(err, ShouldNotBeNil) + }) + }) +} + +func TestDBPointerBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting BSON DBPointer to JSON", t, func() { + Convey("should produce a json.DBPointer", func() { + objId := bson.NewObjectId() + _jObj, err := ConvertBSONValueToJSON(bson.DBPointer{"dbrefnamespace", objId}) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.DBPointer) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.DBPointer{"dbrefnamespace", objId}) + }) + }) +} + +func TestJSCodeBSONToJSON(t *testing.T) { + testutil.VerifyTestType(t, testutil.UnitTestType) + + Convey("Converting BSON Javascript code to JSON", t, func() { + Convey("should produce a json.Javascript", func() { + Convey("without scope if the scope for the BSON Javascript code is nil", func() { + _jObj, err := ConvertBSONValueToJSON(bson.JavaScript{"function() { return null; }", nil}) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.JavaScript) + So(ok, ShouldBeTrue) + + So(jObj, ShouldResemble, json.JavaScript{"function() { return null; }", nil}) + }) + + Convey("with scope if the scope for the BSON Javascript code is non-nil", func() { + _jObj, err := ConvertBSONValueToJSON(bson.JavaScript{"function() { return x; }", bson.M{"x": 2}}) + So(err, ShouldBeNil) + jObj, ok := _jObj.(json.JavaScript) + So(ok, ShouldBeTrue) + So(jObj.Scope.(bson.M)["x"], ShouldEqual, 2) + So(jObj.Code, ShouldEqual, "function() { return x; }") + }) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/date_test.go b/src/mongo/gotools/common/bsonutil/date_test.go new file mode 100644 index 00000000000..a2553219379 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/date_test.go @@ -0,0 +1,169 @@ +package bsonutil + +import ( + "fmt" + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "testing" + "time" +) + +func TestDateValue(t *testing.T) { + + Convey("When converting JSON with Date values", t, func() { + + Convey("works for Date object", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.Date(100), + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(time.Unix(0, int64(100*time.Millisecond))), ShouldBeTrue) + }) + + Convey("works for Date document", func() { + + dates := []string{ + "2006-01-02T15:04:05.000Z", + "2006-01-02T15:04:05.000-0700", + "2006-01-02T15:04:05Z", + "2006-01-02T15:04:05-0700", + "2006-01-02T15:04Z", + "2006-01-02T15:04-0700", + } + + for _, dateString := range dates { + example := fmt.Sprintf(`{ "$date": "%v" }`, dateString) + Convey(fmt.Sprintf("of string ('%v')", example), func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": dateString, + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + // dateString is a valid time format string + date, err := time.Parse(dateString, dateString) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + } + + date := time.Unix(0, int64(time.Duration(1136214245000)*time.Millisecond)) + + Convey(`of $numberLong ('{ "$date": { "$numberLong": "1136214245000" } }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": map[string]interface{}{ + "$numberLong": "1136214245000", + }, + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + + Convey(`of json.Number ('{ "$date": 1136214245000 }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": json.Number("1136214245000"), + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + + Convey(`of numeric int64 ('{ "$date": 1136214245000 }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": int64(1136214245000), + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + + Convey(`of numeric float64 ('{ "$date": 1136214245000 }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": float64(1136214245000), + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + Convey(`of numeric int32 ('{ "$date": 2136800000 }')`, func() { + key := "key" + + date = time.Unix(0, int64(time.Duration(2136800000)*time.Millisecond)) + + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": int32(2136800000), + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + + Convey(`of negative numeric int32 ('{ "$date": -2136800000 }')`, func() { + key := "key" + + date = time.Unix(0, int64(time.Duration(-2136800000)*time.Millisecond)) + + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$date": int32(-2136800000), + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + + jsonValue, ok := jsonMap[key].(time.Time) + So(ok, ShouldBeTrue) + So(jsonValue.Equal(date), ShouldBeTrue) + }) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/marshal_d.go b/src/mongo/gotools/common/bsonutil/marshal_d.go new file mode 100644 index 00000000000..e47eea9c220 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/marshal_d.go @@ -0,0 +1,59 @@ +package bsonutil + +import ( + "bytes" + "fmt" + "github.com/mongodb/mongo-tools/common/json" + "github.com/mongodb/mongo-tools/common/util" + "gopkg.in/mgo.v2/bson" +) + +// MarshalD is a wrapper for bson.D that allows unmarshalling +// of bson.D with preserved order. Necessary for printing +// certain database commands. +type MarshalD bson.D + +// MarshalJSON makes the MarshalD type usable by +// the encoding/json package. +func (md MarshalD) MarshalJSON() ([]byte, error) { + var buff bytes.Buffer + buff.WriteString("{") + for i, item := range md { + key, err := json.Marshal(item.Name) + if err != nil { + return nil, fmt.Errorf("cannot marshal key %v: %v", item.Name, err) + } + val, err := json.Marshal(item.Value) + if err != nil { + return nil, fmt.Errorf("cannot marshal value %v: %v", item.Value, err) + } + buff.Write(key) + buff.WriteString(":") + buff.Write(val) + if i != len(md)-1 { + buff.WriteString(",") + } + } + buff.WriteString("}") + return buff.Bytes(), nil +} + +// MakeSortString takes a bson.D object and converts it to a slice of strings +// that can be used as the input args to mgo's .Sort(...) function. +// For example: +// {a:1, b:-1} -> ["+a", "-b"] +func MakeSortString(sortObj bson.D) ([]string, error) { + sortStrs := make([]string, 0, len(sortObj)) + for _, docElem := range sortObj { + valueAsNumber, err := util.ToFloat64(docElem.Value) + if err != nil { + return nil, err + } + prefix := "+" + if valueAsNumber < 0 { + prefix = "-" + } + sortStrs = append(sortStrs, fmt.Sprintf("%v%v", prefix, docElem.Name)) + } + return sortStrs, nil +} diff --git a/src/mongo/gotools/common/bsonutil/marshal_d_test.go b/src/mongo/gotools/common/bsonutil/marshal_d_test.go new file mode 100644 index 00000000000..dcc3a53415e --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/marshal_d_test.go @@ -0,0 +1,124 @@ +package bsonutil + +import ( + "encoding/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "strings" + "testing" +) + +func TestMarshalDMarshalJSON(t *testing.T) { + + Convey("With a valid bson.D", t, func() { + testD := bson.D{ + {"cool", "rad"}, + {"aaa", 543.2}, + {"I", 0}, + {"E", 0}, + {"map", bson.M{"1": 1, "2": "two"}}, + } + + Convey("wrapping with MarshalD should allow json.Marshal to work", func() { + asJSON, err := json.Marshal(MarshalD(testD)) + So(err, ShouldBeNil) + strJSON := string(asJSON) + + Convey("with order preserved", func() { + So(strings.Index(strJSON, "cool"), ShouldBeLessThan, strings.Index(strJSON, "aaa")) + So(strings.Index(strJSON, "aaa"), ShouldBeLessThan, strings.Index(strJSON, "I")) + So(strings.Index(strJSON, "I"), ShouldBeLessThan, strings.Index(strJSON, "E")) + So(strings.Index(strJSON, "E"), ShouldBeLessThan, strings.Index(strJSON, "map")) + So(strings.Count(strJSON, ","), ShouldEqual, 5) // 4 + 1 from internal map + }) + + Convey("but still usable by the json parser", func() { + var asMap bson.M + err := json.Unmarshal(asJSON, &asMap) + So(err, ShouldBeNil) + + Convey("with types & values preserved", func() { + So(asMap["cool"], ShouldEqual, "rad") + So(asMap["aaa"], ShouldEqual, 543.2) + So(asMap["I"], ShouldEqual, 0) + So(asMap["E"], ShouldEqual, 0) + So(asMap["map"].(map[string]interface{})["1"], ShouldEqual, 1) + So(asMap["map"].(map[string]interface{})["2"], ShouldEqual, "two") + }) + }) + + Convey("putting it inside another map should still be usable by json.Marshal", func() { + _, err := json.Marshal(bson.M{"x": 0, "y": MarshalD(testD)}) + So(err, ShouldBeNil) + }) + }) + }) + + Convey("With en empty bson.D", t, func() { + testD := bson.D{} + + Convey("wrapping with MarshalD should allow json.Marshal to work", func() { + asJSON, err := json.Marshal(MarshalD(testD)) + So(err, ShouldBeNil) + strJSON := string(asJSON) + So(strJSON, ShouldEqual, "{}") + + Convey("but still usable by the json parser", func() { + var asInterface interface{} + err := json.Unmarshal(asJSON, &asInterface) + So(err, ShouldBeNil) + asMap, ok := asInterface.(map[string]interface{}) + So(ok, ShouldBeTrue) + So(len(asMap), ShouldEqual, 0) + }) + }) + }) +} + +func TestFindValueByKey(t *testing.T) { + Convey("Given a bson.D document and a specific key", t, func() { + subDocument := &bson.D{ + bson.DocElem{Name: "field4", Value: "c"}, + } + document := &bson.D{ + bson.DocElem{Name: "field1", Value: "a"}, + bson.DocElem{Name: "field2", Value: "b"}, + bson.DocElem{Name: "field3", Value: subDocument}, + } + Convey("the corresponding value top-level keys should be returned", func() { + value, err := FindValueByKey("field1", document) + So(value, ShouldEqual, "a") + So(err, ShouldBeNil) + }) + Convey("the corresponding value top-level keys with sub-document values should be returned", func() { + value, err := FindValueByKey("field3", document) + So(value, ShouldEqual, subDocument) + So(err, ShouldBeNil) + }) + Convey("for non-existent keys nil and an error should be returned", func() { + value, err := FindValueByKey("field4", document) + So(value, ShouldBeNil) + So(err, ShouldNotBeNil) + }) + }) +} + +func TestEscapedKey(t *testing.T) { + Convey("Given a bson.D document with a key that requires escaping", t, func() { + document := bson.D{ + bson.DocElem{Name: `foo"bar`, Value: "a"}, + } + Convey("it can be marshaled without error", func() { + asJSON, err := json.Marshal(MarshalD(document)) + So(err, ShouldBeNil) + Convey("and subsequently unmarshaled without error", func() { + var asMap bson.M + err := json.Unmarshal(asJSON, &asMap) + So(err, ShouldBeNil) + Convey("with the original value being correctly found with the unescaped key", func() { + So(asMap[`foo"bar`], ShouldEqual, "a") + }) + }) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/maxkey_test.go b/src/mongo/gotools/common/bsonutil/maxkey_test.go new file mode 100644 index 00000000000..8676c449e32 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/maxkey_test.go @@ -0,0 +1,38 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "testing" +) + +func TestMaxKeyValue(t *testing.T) { + + Convey("When converting JSON with MaxKey values", t, func() { + + Convey("works for MaxKey literal", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.MaxKey{}, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.MaxKey) + }) + + Convey(`works for MaxKey document ('{ "$maxKey": 1 }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$maxKey": 1, + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.MaxKey) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/minkey_test.go b/src/mongo/gotools/common/bsonutil/minkey_test.go new file mode 100644 index 00000000000..149bcd42796 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/minkey_test.go @@ -0,0 +1,38 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "testing" +) + +func TestMinKeyValue(t *testing.T) { + + Convey("When converting JSON with MinKey values", t, func() { + + Convey("works for MinKey literal", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.MinKey{}, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.MinKey) + }) + + Convey(`works for MinKey document ('{ "$minKey": 1 }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$minKey": 1, + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.MinKey) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/number.go b/src/mongo/gotools/common/bsonutil/number.go new file mode 100644 index 00000000000..044edbc5274 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/number.go @@ -0,0 +1,18 @@ +package bsonutil + +import ( + "fmt" + "reflect" +) + +var floatType = reflect.TypeOf(float64(0)) + +func getFloat(unk interface{}) (float64, error) { + v := reflect.ValueOf(unk) + v = reflect.Indirect(v) + if !v.Type().ConvertibleTo(floatType) { + return 0, fmt.Errorf("cannot convert %v to float64", v.Type()) + } + fv := v.Convert(floatType) + return fv.Float(), nil +} diff --git a/src/mongo/gotools/common/bsonutil/numberint_test.go b/src/mongo/gotools/common/bsonutil/numberint_test.go new file mode 100644 index 00000000000..8dc368b6668 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/numberint_test.go @@ -0,0 +1,37 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +func TestNumberIntValue(t *testing.T) { + + Convey("When converting JSON with NumberInt values", t, func() { + + Convey("works for NumberInt constructor", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.NumberInt(42), + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldEqual, int32(42)) + }) + + Convey(`works for NumberInt document ('{ "$numberInt": "42" }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$numberInt": "42", + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldEqual, int32(42)) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/numberlong_test.go b/src/mongo/gotools/common/bsonutil/numberlong_test.go new file mode 100644 index 00000000000..d2706b61847 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/numberlong_test.go @@ -0,0 +1,37 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +func TestNumberLongValue(t *testing.T) { + + Convey("When converting JSON with NumberLong values", t, func() { + + Convey("works for NumberLong constructor", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.NumberLong(42), + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldEqual, int64(42)) + }) + + Convey(`works for NumberLong document ('{ "$numberLong": "42" }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$numberLong": "42", + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldEqual, int64(42)) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/objectid_test.go b/src/mongo/gotools/common/bsonutil/objectid_test.go new file mode 100644 index 00000000000..bc1df9d6b4a --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/objectid_test.go @@ -0,0 +1,38 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "testing" +) + +func TestObjectIdValue(t *testing.T) { + + Convey("When converting JSON with ObjectId values", t, func() { + + Convey("works for ObjectId constructor", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.ObjectId("0123456789abcdef01234567"), + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldEqual, bson.ObjectIdHex("0123456789abcdef01234567")) + }) + + Convey(`works for ObjectId document ('{ "$oid": "0123456789abcdef01234567" }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$oid": "0123456789abcdef01234567", + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldEqual, bson.ObjectIdHex("0123456789abcdef01234567")) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/regexp_test.go b/src/mongo/gotools/common/bsonutil/regexp_test.go new file mode 100644 index 00000000000..fe4fd350323 --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/regexp_test.go @@ -0,0 +1,66 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "testing" +) + +func TestRegExpValue(t *testing.T) { + + Convey("When converting JSON with RegExp values", t, func() { + + Convey("works for RegExp constructor", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.RegExp{"foo", "i"}, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.RegEx{"foo", "i"}) + }) + + Convey(`works for RegExp document ('{ "$regex": "foo", "$options": "i" }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$regex": "foo", + "$options": "i", + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.RegEx{"foo", "i"}) + }) + + Convey(`can use multiple options ('{ "$regex": "bar", "$options": "gims" }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$regex": "bar", + "$options": "gims", + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.RegEx{"bar", "gims"}) + }) + + Convey(`fails for an invalid option ('{ "$regex": "baz", "$options": "y" }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$regex": "baz", + "$options": "y", + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldNotBeNil) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/timestamp_test.go b/src/mongo/gotools/common/bsonutil/timestamp_test.go new file mode 100644 index 00000000000..05899febc2d --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/timestamp_test.go @@ -0,0 +1,43 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "testing" +) + +func TestTimestampValue(t *testing.T) { + + Convey("When converting JSON with Timestamp values", t, func() { + testTS := bson.MongoTimestamp(123456<<32 | 55) + + Convey("works for Timestamp literal", func() { + + jsonMap := map[string]interface{}{ + "ts": json.Timestamp{123456, 55}, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap["ts"], ShouldEqual, testTS) + }) + + Convey(`works for Timestamp document`, func() { + Convey(`{"ts":{"$timestamp":{"t":123456, "i":55}}}`, func() { + jsonMap := map[string]interface{}{ + "ts": map[string]interface{}{ + "$timestamp": map[string]interface{}{ + "t": 123456.0, + "i": 55.0, + }, + }, + } + + bsonMap, err := ConvertJSONValueToBSON(jsonMap) + So(err, ShouldBeNil) + So(bsonMap.(map[string]interface{})["ts"], ShouldEqual, testTS) + }) + }) + }) +} diff --git a/src/mongo/gotools/common/bsonutil/undefined_test.go b/src/mongo/gotools/common/bsonutil/undefined_test.go new file mode 100644 index 00000000000..0126e426ebc --- /dev/null +++ b/src/mongo/gotools/common/bsonutil/undefined_test.go @@ -0,0 +1,38 @@ +package bsonutil + +import ( + "github.com/mongodb/mongo-tools/common/json" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/mgo.v2/bson" + "testing" +) + +func TestUndefinedValue(t *testing.T) { + + Convey("When converting JSON with undefined values", t, func() { + + Convey("works for undefined literal", func() { + key := "key" + jsonMap := map[string]interface{}{ + key: json.Undefined{}, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.Undefined) + }) + + Convey(`works for undefined document ('{ "$undefined": true }')`, func() { + key := "key" + jsonMap := map[string]interface{}{ + key: map[string]interface{}{ + "$undefined": true, + }, + } + + err := ConvertJSONDocumentToBSON(jsonMap) + So(err, ShouldBeNil) + So(jsonMap[key], ShouldResemble, bson.Undefined) + }) + }) +} |