summaryrefslogtreecommitdiff
path: root/src/mongo/gotools/common/bsonutil
diff options
context:
space:
mode:
Diffstat (limited to 'src/mongo/gotools/common/bsonutil')
-rw-r--r--src/mongo/gotools/common/bsonutil/bsonutil.go416
-rw-r--r--src/mongo/gotools/common/bsonutil/converter.go388
-rw-r--r--src/mongo/gotools/common/bsonutil/converter_test.go345
-rw-r--r--src/mongo/gotools/common/bsonutil/date_test.go169
-rw-r--r--src/mongo/gotools/common/bsonutil/marshal_d.go59
-rw-r--r--src/mongo/gotools/common/bsonutil/marshal_d_test.go124
-rw-r--r--src/mongo/gotools/common/bsonutil/maxkey_test.go38
-rw-r--r--src/mongo/gotools/common/bsonutil/minkey_test.go38
-rw-r--r--src/mongo/gotools/common/bsonutil/number.go18
-rw-r--r--src/mongo/gotools/common/bsonutil/numberint_test.go37
-rw-r--r--src/mongo/gotools/common/bsonutil/numberlong_test.go37
-rw-r--r--src/mongo/gotools/common/bsonutil/objectid_test.go38
-rw-r--r--src/mongo/gotools/common/bsonutil/regexp_test.go66
-rw-r--r--src/mongo/gotools/common/bsonutil/timestamp_test.go43
-rw-r--r--src/mongo/gotools/common/bsonutil/undefined_test.go38
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)
+ })
+ })
+}