Skip to content

Commit 20a9223

Browse files
authored
GODRIVER-2161 Add NewSingleResultFromDocument and NewCursorFromDocuments functions (#806)
1 parent 5982664 commit 20a9223

File tree

6 files changed

+321
-0
lines changed

6 files changed

+321
-0
lines changed

mongo/cursor.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"go.mongodb.org/mongo-driver/bson"
1717
"go.mongodb.org/mongo-driver/bson/bsoncodec"
18+
"go.mongodb.org/mongo-driver/x/bsonx"
1819
"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
1920
"go.mongodb.org/mongo-driver/x/mongo/driver"
2021
"go.mongodb.org/mongo-driver/x/mongo/driver/session"
@@ -67,6 +68,47 @@ func newEmptyCursor() *Cursor {
6768
return &Cursor{bc: driver.NewEmptyBatchCursor()}
6869
}
6970

71+
// NewCursorFromDocuments creates a new Cursor pre-loaded with the provided documents, error and registry. If no registry is provided,
72+
// bson.DefaultRegistry will be used.
73+
//
74+
// The documents parameter must be a slice of documents. The slice may be nil or empty, but all elements must be non-nil.
75+
func NewCursorFromDocuments(documents []interface{}, err error, registry *bsoncodec.Registry) (*Cursor, error) {
76+
if registry == nil {
77+
registry = bson.DefaultRegistry
78+
}
79+
80+
// Convert documents slice to a sequence-style byte array.
81+
var docsBytes []byte
82+
for _, doc := range documents {
83+
switch t := doc.(type) {
84+
case nil:
85+
return nil, ErrNilDocument
86+
case bsonx.Doc:
87+
doc = t.Copy()
88+
case []byte:
89+
// Slight optimization so we'll just use MarshalBSON and not go through the codec machinery.
90+
doc = bson.Raw(t)
91+
}
92+
var marshalErr error
93+
docsBytes, marshalErr = bson.MarshalAppendWithRegistry(registry, docsBytes, doc)
94+
if marshalErr != nil {
95+
return nil, marshalErr
96+
}
97+
}
98+
99+
c := &Cursor{
100+
bc: driver.NewBatchCursorFromDocuments(docsBytes),
101+
registry: registry,
102+
err: err,
103+
}
104+
105+
// Initialize batch and batchLength here. The underlying batch cursor will be preloaded with the
106+
// provided contents, and thus already has a batch before calls to Next/TryNext.
107+
c.batch = c.bc.Batch()
108+
c.batchLength = c.bc.Batch().DocumentCount()
109+
return c, nil
110+
}
111+
70112
// ID returns the ID of this cursor, or 0 if the cursor has been closed or exhausted.
71113
func (c *Cursor) ID() int64 { return c.bc.ID() }
72114

mongo/cursor_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package mongo
88

99
import (
1010
"context"
11+
"fmt"
1112
"testing"
1213

1314
"go.mongodb.org/mongo-driver/bson"
@@ -179,3 +180,57 @@ func TestCursor(t *testing.T) {
179180
})
180181
})
181182
}
183+
184+
func TestNewCursorFromDocuments(t *testing.T) {
185+
// Mock documents returned by Find in a Cursor.
186+
t.Run("mock Find", func(t *testing.T) {
187+
findResult := []interface{}{
188+
bson.D{{"_id", 0}, {"foo", "bar"}},
189+
bson.D{{"_id", 1}, {"baz", "qux"}},
190+
bson.D{{"_id", 2}, {"quux", "quuz"}},
191+
}
192+
cur, err := NewCursorFromDocuments(findResult, nil, nil)
193+
assert.Nil(t, err, "NewCursorFromDocuments error: %v", err)
194+
195+
// Assert that decoded documents are as expected.
196+
var i int
197+
for cur.Next(context.Background()) {
198+
docBytes, err := bson.Marshal(findResult[i])
199+
assert.Nil(t, err, "Marshal error: %v", err)
200+
expectedDecoded := bson.Raw(docBytes)
201+
202+
var decoded bson.Raw
203+
err = cur.Decode(&decoded)
204+
assert.Nil(t, err, "Decode error: %v", err)
205+
assert.Equal(t, expectedDecoded, decoded,
206+
"expected decoded document %v of Cursor to be %v, got %v",
207+
i, expectedDecoded, decoded)
208+
i++
209+
}
210+
assert.Equal(t, 3, i, "expected 3 calls to cur.Next, got %v", i)
211+
212+
// Check for error on Cursor.
213+
assert.Nil(t, cur.Err(), "Cursor error: %v", cur.Err())
214+
215+
// Assert that a call to cur.Close will not fail.
216+
err = cur.Close(context.Background())
217+
assert.Nil(t, err, "Close error: %v", err)
218+
})
219+
220+
// Mock an error in a Cursor.
221+
t.Run("mock Find with error", func(t *testing.T) {
222+
mockErr := fmt.Errorf("mock error")
223+
findResult := []interface{}{bson.D{{"_id", 0}, {"foo", "bar"}}}
224+
cur, err := NewCursorFromDocuments(findResult, mockErr, nil)
225+
assert.Nil(t, err, "NewCursorFromDocuments error: %v", err)
226+
227+
// Assert that a call to Next will return false because of existing error.
228+
next := cur.Next(context.Background())
229+
assert.False(t, next, "expected call to Next to return false, got true")
230+
231+
// Check for error on Cursor.
232+
assert.NotNil(t, cur.Err(), "expected Cursor error, got nil")
233+
assert.Equal(t, mockErr, cur.Err(), "expected Cursor error %v, got %v",
234+
mockErr, cur.Err())
235+
})
236+
}

mongo/integration/mock_find_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (C) MongoDB, Inc. 2021-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package integration
8+
9+
import (
10+
"context"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
"go.mongodb.org/mongo-driver/bson"
15+
"go.mongodb.org/mongo-driver/bson/bsoncodec"
16+
"go.mongodb.org/mongo-driver/mongo"
17+
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
18+
"go.mongodb.org/mongo-driver/mongo/options"
19+
)
20+
21+
// finder is an object that implements FindOne and Find.
22+
type finder interface {
23+
FindOne(ctx context.Context, filter interface{}, opts ...*options.FindOneOptions) *mongo.SingleResult
24+
Find(context.Context, interface{}, ...*options.FindOptions) (*mongo.Cursor, error)
25+
}
26+
27+
// mockFinder implements finder.
28+
type mockFinder struct {
29+
docs []interface{}
30+
err error
31+
registry *bsoncodec.Registry
32+
}
33+
34+
// FindOne mocks a findOne operation using NewSingleResultFromDocument.
35+
func (mf *mockFinder) FindOne(_ context.Context, _ interface{}, _ ...*options.FindOneOptions) *mongo.SingleResult {
36+
return mongo.NewSingleResultFromDocument(mf.docs[0], mf.err, mf.registry)
37+
}
38+
39+
// Find mocks a find operation using NewCursorFromDocuments.
40+
func (mf *mockFinder) Find(context.Context, interface{}, ...*options.FindOptions) (*mongo.Cursor, error) {
41+
return mongo.NewCursorFromDocuments(mf.docs, mf.err, mf.registry)
42+
}
43+
44+
// ShopItem is an item with an associated ID and price.
45+
type ShopItem struct {
46+
ID int `bson:"id"`
47+
Price float64 `bson:"price"`
48+
}
49+
50+
// getItem is an example function using the interface finder to test the mocking of SingleResult.
51+
func getItem(f finder, id int) (*ShopItem, error) {
52+
res := f.FindOne(context.Background(), bson.D{{"id", id}})
53+
var item ShopItem
54+
err := res.Decode(&item)
55+
return &item, err
56+
}
57+
58+
// getItems is an example function using the interface finder to test the mocking of Cursor.
59+
func getItems(f finder) ([]ShopItem, error) {
60+
cur, err := f.Find(context.Background(), bson.D{})
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
var items []ShopItem
66+
err = cur.All(context.Background(), &items)
67+
return items, err
68+
}
69+
70+
func TestMockFind(t *testing.T) {
71+
mt := mtest.New(t, mtest.NewOptions().CreateClient(false))
72+
defer mt.Close()
73+
74+
insertItems := []interface{}{
75+
ShopItem{ID: 0, Price: 1.5},
76+
ShopItem{ID: 1, Price: 5.7},
77+
ShopItem{ID: 2, Price: 0.25},
78+
}
79+
insertItem := []interface{}{ShopItem{ID: 1, Price: 5.7}}
80+
81+
mt.Run("mongo.Collection can be passed as interface", func(mt *mtest.T) {
82+
// Actually insert documents to collection.
83+
_, err := mt.Coll.InsertMany(mtest.Background, insertItems)
84+
assert.Nil(mt, err, "InsertMany error: %v", err)
85+
86+
// Assert that FindOne behaves as expected.
87+
shopItem, err := getItem(mt.Coll, 1)
88+
assert.Nil(mt, err, "getItem error: %v", err)
89+
assert.Equal(mt, 1, shopItem.ID)
90+
assert.Equal(mt, 5.7, shopItem.Price)
91+
92+
// Assert that Find behaves as expected.
93+
shopItems, err := getItems(mt.Coll)
94+
assert.Nil(mt, err, "getItems error: %v", err)
95+
for i, shopItem := range shopItems {
96+
expectedItem := insertItems[i].(ShopItem)
97+
assert.Equal(mt, expectedItem.ID, shopItem.ID)
98+
assert.Equal(mt, expectedItem.Price, shopItem.Price)
99+
}
100+
})
101+
102+
mt.Run("FindOne can be mocked", func(mt *mtest.T) {
103+
// Mock a FindOne result with mockFinder.
104+
mf := &mockFinder{docs: insertItem, err: nil, registry: nil}
105+
106+
// Assert that FindOne behaves as expected.
107+
shopItem, err := getItem(mf, 1)
108+
assert.Nil(mt, err, "getItem error: %v", err)
109+
assert.Equal(mt, 1, shopItem.ID)
110+
assert.Equal(mt, 5.7, shopItem.Price)
111+
})
112+
113+
mt.Run("Find can be mocked", func(mt *mtest.T) {
114+
// Mock a Find result with mockFinder.
115+
mf := &mockFinder{docs: insertItems, err: nil, registry: nil}
116+
117+
// Assert that Find behaves as expected.
118+
shopItems, err := getItems(mf)
119+
assert.Nil(mt, err, "getItems error: %v", err)
120+
for i, shopItem := range shopItems {
121+
expectedItem := insertItems[i].(ShopItem)
122+
assert.Equal(mt, expectedItem.ID, shopItem.ID)
123+
assert.Equal(mt, expectedItem.Price, shopItem.Price)
124+
}
125+
})
126+
}

mongo/single_result.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ type SingleResult struct {
2828
reg *bsoncodec.Registry
2929
}
3030

31+
// NewSingleResultFromDocument creates a SingleResult with the provided error, registry, and an underlying Cursor pre-loaded with
32+
// the provided document, error and registry. If no registry is provided, bson.DefaultRegistry will be used. If an error distinct
33+
// from the one provided occurs during creation of the SingleResult, that error will be stored on the returned SingleResult.
34+
//
35+
// The document parameter must be a non-nil document.
36+
func NewSingleResultFromDocument(document interface{}, err error, registry *bsoncodec.Registry) *SingleResult {
37+
if document == nil {
38+
return &SingleResult{err: ErrNilDocument}
39+
}
40+
if registry == nil {
41+
registry = bson.DefaultRegistry
42+
}
43+
44+
cur, createErr := NewCursorFromDocuments([]interface{}{document}, err, registry)
45+
if createErr != nil {
46+
return &SingleResult{err: createErr}
47+
}
48+
49+
return &SingleResult{
50+
cur: cur,
51+
err: err,
52+
reg: registry,
53+
}
54+
}
55+
3156
// Decode will unmarshal the document represented by this SingleResult into v. If there was an error from the operation
3257
// that created this SingleResult, that error will be returned. If the operation returned no documents, Decode will
3358
// return ErrNoDocuments.
@@ -71,6 +96,7 @@ func (sr *SingleResult) setRdrContents() error {
7196
return nil
7297
case sr.cur != nil:
7398
defer sr.cur.Close(context.TODO())
99+
74100
if !sr.cur.Next(context.TODO()) {
75101
if err := sr.cur.Err(); err != nil {
76102
return err

mongo/single_result_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
package mongo
88

99
import (
10+
"context"
1011
"errors"
12+
"fmt"
1113
"testing"
1214

1315
"go.mongodb.org/mongo-driver/bson"
@@ -49,3 +51,58 @@ func TestSingleResult(t *testing.T) {
4951
assert.Equal(t, ErrNoDocuments, sr.Err(), "expected error %v, got %v", ErrNoDocuments, sr.Err())
5052
})
5153
}
54+
55+
func TestNewSingleResultFromDocument(t *testing.T) {
56+
// Mock a document returned by FindOne in SingleResult.
57+
t.Run("mock FindOne", func(t *testing.T) {
58+
findOneResult := bson.D{{"_id", 2}, {"foo", "bar"}}
59+
res := NewSingleResultFromDocument(findOneResult, nil, nil)
60+
61+
// Assert that first, decoded document is as expected.
62+
findOneResultBytes, err := bson.Marshal(findOneResult)
63+
assert.Nil(t, err, "Marshal error: %v", err)
64+
expectedDecoded := bson.Raw(findOneResultBytes)
65+
decoded, err := res.DecodeBytes()
66+
assert.Nil(t, err, "DecodeBytes error: %v", err)
67+
assert.Equal(t, expectedDecoded, decoded,
68+
"expected decoded SingleResult to be %v, got %v", expectedDecoded, decoded)
69+
70+
// Assert that RDR contents are set correctly after Decode.
71+
assert.NotNil(t, res.rdr, "expected non-nil rdr contents")
72+
assert.Equal(t, expectedDecoded, res.rdr,
73+
"expected RDR contents to be %v, got %v", expectedDecoded, res.rdr)
74+
75+
// Assert that a call to cur.Next will return false, as there was only one document in
76+
// the slice passed to NewSingleResultFromDocument.
77+
next := res.cur.Next(context.Background())
78+
assert.False(t, next, "expected call to Next to return false, got true")
79+
80+
// Check for error on SingleResult.
81+
assert.Nil(t, res.Err(), "SingleResult error: %v", res.Err())
82+
83+
// Assert that a call to cur.Close will not fail.
84+
err = res.cur.Close(context.Background())
85+
assert.Nil(t, err, "Close error: %v", err)
86+
})
87+
88+
// Mock an error in SingleResult.
89+
t.Run("mock FindOne with error", func(t *testing.T) {
90+
mockErr := fmt.Errorf("mock error")
91+
res := NewSingleResultFromDocument(bson.D{}, mockErr, nil)
92+
93+
// Assert that decoding returns the mocked error.
94+
_, err := res.DecodeBytes()
95+
assert.NotNil(t, err, "expected DecodeBytes error, got nil")
96+
assert.Equal(t, mockErr, err, "expected error %v, got %v", mockErr, err)
97+
98+
// Check for error on SingleResult.
99+
assert.NotNil(t, res.Err(), "expected SingleResult error, got nil")
100+
assert.Equal(t, mockErr, res.Err(), "expected SingleResult error %v, got %v",
101+
mockErr, res.Err())
102+
103+
// Assert that error is propagated to underlying cursor.
104+
assert.NotNil(t, res.cur.err, "expected underlying cursor, got nil")
105+
assert.Equal(t, mockErr, res.cur.err, "expected underlying cursor %v, got %v",
106+
mockErr, res.cur.err)
107+
})
108+
}

x/mongo/driver/batch_cursor.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,21 @@ func NewEmptyBatchCursor() *BatchCursor {
184184
return &BatchCursor{currentBatch: new(bsoncore.DocumentSequence)}
185185
}
186186

187+
// NewBatchCursorFromDocuments returns a batch cursor with current batch set to a sequence-style
188+
// DocumentSequence containing the provided documents.
189+
func NewBatchCursorFromDocuments(documents []byte) *BatchCursor {
190+
return &BatchCursor{
191+
currentBatch: &bsoncore.DocumentSequence{
192+
Data: documents,
193+
Style: bsoncore.SequenceStyle,
194+
},
195+
// BatchCursors created with this function have no associated ID nor server, so no getMore
196+
// calls will be made.
197+
id: 0,
198+
server: nil,
199+
}
200+
}
201+
187202
// ID returns the cursor ID for this batch cursor.
188203
func (bc *BatchCursor) ID() int64 {
189204
return bc.id

0 commit comments

Comments
 (0)