Skip to content

Commit 2365c36

Browse files
committed
Add mongo/replaceopt
GODRIVER-272 Change-Id: Ifed06e1f8ede8e292e431200360a79fed2ae26d3
1 parent 3660569 commit 2365c36

File tree

5 files changed

+474
-9
lines changed

5 files changed

+474
-9
lines changed

mongo/collection.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/mongodb/mongo-go-driver/mongo/aggregateopt"
2323
"github.com/mongodb/mongo-go-driver/mongo/findopt"
2424
"github.com/mongodb/mongo-go-driver/mongo/insertopt"
25+
"github.com/mongodb/mongo-go-driver/mongo/replaceopt"
2526
"github.com/mongodb/mongo-go-driver/mongo/updateopt"
2627
)
2728

@@ -400,7 +401,7 @@ func (coll *Collection) UpdateMany(ctx context.Context, filter interface{}, upda
400401
// parameter into a *bson.Document. See TransformDocument for the list of
401402
// valid types for filter and replacement.
402403
func (coll *Collection) ReplaceOne(ctx context.Context, filter interface{},
403-
replacement interface{}, opts ...option.ReplaceOptioner) (*UpdateResult, error) {
404+
replacement interface{}, opts ...replaceopt.Replace) (*UpdateResult, error) {
404405

405406
if ctx == nil {
406407
ctx = context.Background()
@@ -420,8 +421,13 @@ func (coll *Collection) ReplaceOne(ctx context.Context, filter interface{},
420421
return nil, errors.New("replacement document cannot contains keys beginning with '$")
421422
}
422423

424+
repOpts, err := replaceopt.BundleReplace(opts...).Unbundle(true)
425+
if err != nil {
426+
return nil, err
427+
}
428+
423429
updateOptions := make([]option.UpdateOptioner, 0, len(opts))
424-
for _, opt := range opts {
430+
for _, opt := range repOpts {
425431
updateOptions = append(updateOptions, opt)
426432
}
427433

mongo/collection_internal_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/mongodb/mongo-go-driver/mongo/aggregateopt"
2121
"github.com/mongodb/mongo-go-driver/mongo/findopt"
2222
"github.com/mongodb/mongo-go-driver/mongo/insertopt"
23+
"github.com/mongodb/mongo-go-driver/mongo/replaceopt"
2324
"github.com/mongodb/mongo-go-driver/mongo/updateopt"
2425
"github.com/stretchr/testify/assert"
2526
"github.com/stretchr/testify/require"
@@ -898,7 +899,7 @@ func TestCollection_ReplaceOne_upsert(t *testing.T) {
898899
filter := bson.NewDocument(bson.EC.Int32("x", 0))
899900
replacement := bson.NewDocument(bson.EC.Int32("y", 1))
900901

901-
result, err := coll.ReplaceOne(context.Background(), filter, replacement, Opt.Upsert(true))
902+
result, err := coll.ReplaceOne(context.Background(), filter, replacement, replaceopt.Upsert(true))
902903
require.Nil(t, err)
903904
require.Equal(t, result.MatchedCount, int64(0))
904905
require.Equal(t, result.ModifiedCount, int64(0))
@@ -951,9 +952,8 @@ func TestCollection_ReplaceOne_WriteConcernError(t *testing.T) {
951952
update := bson.NewDocument(bson.EC.Double("pi", 3.14159))
952953
coll := createTestCollection(t, nil, nil)
953954

954-
optwc, err := Opt.WriteConcern(writeconcern.New(writeconcern.W(25)))
955-
require.NoError(t, err)
956-
_, err = coll.ReplaceOne(context.Background(), filter, update, optwc)
955+
optwc := replaceopt.WriteConcern(writeconcern.New(writeconcern.W(25)))
956+
_, err := coll.ReplaceOne(context.Background(), filter, update, optwc)
957957
got, ok := err.(WriteConcernError)
958958
if !ok {
959959
t.Errorf("Did not receive correct type of error. got %T; want %T", err, WriteConcernError{})

mongo/crud_spec_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/mongodb/mongo-go-driver/internal/testutil/helpers"
1717
"github.com/mongodb/mongo-go-driver/mongo/aggregateopt"
1818
"github.com/mongodb/mongo-go-driver/mongo/findopt"
19+
"github.com/mongodb/mongo-go-driver/mongo/replaceopt"
1920
"github.com/mongodb/mongo-go-driver/mongo/updateopt"
2021
"github.com/stretchr/testify/require"
2122
)
@@ -654,14 +655,14 @@ func replaceOneTest(t *testing.T, coll *Collection, test *testCase) {
654655
replaceFloatsWithInts(filter)
655656
replaceFloatsWithInts(replacement)
656657

657-
var opts []option.ReplaceOptioner
658+
var opts []replaceopt.Replace
658659

659660
if upsert, found := test.Operation.Arguments["upsert"]; found {
660-
opts = append(opts, Opt.Upsert(upsert.(bool)))
661+
opts = append(opts, replaceopt.Upsert(upsert.(bool)))
661662
}
662663

663664
if collation, found := test.Operation.Arguments["collation"]; found {
664-
opts = append(opts, Opt.Collation(collationFromMap(collation.(map[string]interface{}))))
665+
opts = append(opts, replaceopt.Collation(collationFromMap(collation.(map[string]interface{}))))
665666
}
666667

667668
actual, err := coll.ReplaceOne(context.Background(), filter, replacement, opts...)

mongo/replaceopt/replaceopt.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
// Copyright (C) MongoDB, Inc. 2017-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 replaceopt
8+
9+
import (
10+
"reflect"
11+
12+
"github.com/mongodb/mongo-go-driver/core/option"
13+
"github.com/mongodb/mongo-go-driver/core/writeconcern"
14+
)
15+
16+
var replaceBundle = new(ReplaceBundle)
17+
18+
// Replace is options for the replace() function
19+
type Replace interface {
20+
replace()
21+
ConvertOption() option.ReplaceOptioner
22+
}
23+
24+
// ReplaceBundle is a bundle of Replace options
25+
type ReplaceBundle struct {
26+
option Replace
27+
next *ReplaceBundle
28+
}
29+
30+
// Implement the Replace interface
31+
func (rb *ReplaceBundle) replace() {}
32+
33+
// ConvertOption implements the Replace interface
34+
func (rb *ReplaceBundle) ConvertOption() option.ReplaceOptioner { return nil }
35+
36+
// BundleReplace bundles Replace options
37+
func BundleReplace(opts ...Replace) *ReplaceBundle {
38+
head := replaceBundle
39+
40+
for _, opt := range opts {
41+
newBundle := ReplaceBundle{
42+
option: opt,
43+
next: head,
44+
}
45+
46+
head = &newBundle
47+
}
48+
49+
return head
50+
}
51+
52+
// BypassDocumentValidation adds an option to allow the write to opt-out of document-level validation.
53+
func (rb *ReplaceBundle) BypassDocumentValidation(b bool) *ReplaceBundle {
54+
bundle := &ReplaceBundle{
55+
option: BypassDocumentValidation(b),
56+
next: rb,
57+
}
58+
59+
return bundle
60+
}
61+
62+
// Collation adds an option to specify a Collation.
63+
func (rb *ReplaceBundle) Collation(c *option.Collation) *ReplaceBundle {
64+
bundle := &ReplaceBundle{
65+
option: Collation(c),
66+
next: rb,
67+
}
68+
69+
return bundle
70+
}
71+
72+
// Upsert adds an option to specify whether to insert a new document if it does not exist
73+
func (rb *ReplaceBundle) Upsert(b bool) *ReplaceBundle {
74+
bundle := &ReplaceBundle{
75+
option: Upsert(b),
76+
next: rb,
77+
}
78+
79+
return bundle
80+
}
81+
82+
// WriteConcern adds an option to specify the write concern.
83+
func (rb *ReplaceBundle) WriteConcern(wc *writeconcern.WriteConcern) *ReplaceBundle {
84+
bundle := &ReplaceBundle{
85+
option: WriteConcern(wc),
86+
next: rb,
87+
}
88+
89+
return bundle
90+
}
91+
92+
// String implements the Stringer interface.
93+
func (rb *ReplaceBundle) String() string {
94+
if rb == nil {
95+
return ""
96+
}
97+
98+
str := ""
99+
for head := rb; head != nil && head.option != nil; head = head.next {
100+
if converted, ok := head.option.(*ReplaceBundle); ok {
101+
str += converted.String()
102+
continue
103+
}
104+
105+
str += head.option.ConvertOption().String() + "\n"
106+
}
107+
108+
return str
109+
}
110+
111+
// Calculates the total length of a bundle, accounting for nested bundles.
112+
func (rb *ReplaceBundle) bundleLength() int {
113+
if rb == nil {
114+
return 0
115+
}
116+
117+
bundleLen := 0
118+
for ; rb != nil && rb.option != nil; rb = rb.next {
119+
if converted, ok := rb.option.(*ReplaceBundle); ok {
120+
// nested bundle
121+
bundleLen += converted.bundleLength()
122+
continue
123+
}
124+
125+
bundleLen++
126+
}
127+
128+
return bundleLen
129+
}
130+
131+
// Unbundle transforms a bundle into a slice of options, optionally deduplicating
132+
func (rb *ReplaceBundle) Unbundle(deduplicate bool) ([]option.ReplaceOptioner, error) {
133+
134+
options, err := rb.unbundle()
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
if !deduplicate {
140+
return options, nil
141+
}
142+
143+
// iterate backwards and make dedup slice
144+
optionsSet := make(map[reflect.Type]struct{})
145+
146+
for i := len(options) - 1; i >= 0; i-- {
147+
currOption := options[i]
148+
optionType := reflect.TypeOf(currOption)
149+
150+
if _, ok := optionsSet[optionType]; ok {
151+
// option already found
152+
options = append(options[:i], options[i+1:]...)
153+
continue
154+
}
155+
156+
optionsSet[optionType] = struct{}{}
157+
}
158+
159+
return options, nil
160+
}
161+
162+
// Helper that recursively unwraps bundle into slice of options
163+
func (rb *ReplaceBundle) unbundle() ([]option.ReplaceOptioner, error) {
164+
if rb == nil {
165+
return nil, nil
166+
}
167+
168+
listLen := rb.bundleLength()
169+
170+
options := make([]option.ReplaceOptioner, listLen)
171+
index := listLen - 1
172+
173+
for listHead := rb; listHead != nil && listHead.option != nil; listHead = listHead.next {
174+
// if the current option is a nested bundle, Unbundle it and add its options to the current array
175+
if converted, ok := listHead.option.(*ReplaceBundle); ok {
176+
nestedOptions, err := converted.unbundle()
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
// where to start inserting nested options
182+
startIndex := index - len(nestedOptions) + 1
183+
184+
// add nested options in order
185+
for _, nestedOp := range nestedOptions {
186+
options[startIndex] = nestedOp
187+
startIndex++
188+
}
189+
index -= len(nestedOptions)
190+
continue
191+
}
192+
193+
options[index] = listHead.option.ConvertOption()
194+
index--
195+
}
196+
197+
return options, nil
198+
199+
}
200+
201+
// BypassDocumentValidation allows the write to opt-out of document-level validation.
202+
func BypassDocumentValidation(b bool) OptBypassDocumentValidation {
203+
return OptBypassDocumentValidation(b)
204+
}
205+
206+
// Collation specifies a Collation.
207+
func Collation(c *option.Collation) OptCollation {
208+
return OptCollation{Collation: c}
209+
}
210+
211+
// Upsert specifies whether to insert a new document if it does not exist
212+
func Upsert(b bool) OptUpsert {
213+
return OptUpsert(b)
214+
}
215+
216+
// WriteConcern specifies the write concern.
217+
func WriteConcern(wc *writeconcern.WriteConcern) OptWriteConcern {
218+
return OptWriteConcern{
219+
WriteConcern: wc,
220+
}
221+
}
222+
223+
// OptBypassDocumentValidation allows the write to opt-out of document-level validation.
224+
type OptBypassDocumentValidation option.OptBypassDocumentValidation
225+
226+
func (OptBypassDocumentValidation) replace() {}
227+
228+
// ConvertOption implements the Replace interface
229+
func (opt OptBypassDocumentValidation) ConvertOption() option.ReplaceOptioner {
230+
return option.OptBypassDocumentValidation(opt)
231+
}
232+
233+
// OptCollation specifies a Collation.
234+
type OptCollation option.OptCollation
235+
236+
func (OptCollation) replace() {}
237+
238+
// ConvertOption implements the replace interface
239+
func (opt OptCollation) ConvertOption() option.ReplaceOptioner {
240+
return option.OptCollation(opt)
241+
}
242+
243+
// OptUpsert specifies whether to insert a new document if it does not exist
244+
type OptUpsert option.OptUpsert
245+
246+
func (OptUpsert) replace() {}
247+
248+
// ConvertOption implements the Replace interface
249+
func (opt OptUpsert) ConvertOption() option.ReplaceOptioner {
250+
return option.OptUpsert(opt)
251+
}
252+
253+
// OptWriteConcern specifies the write concern.
254+
type OptWriteConcern option.OptWriteConcern
255+
256+
func (OptWriteConcern) replace() {}
257+
258+
// ConvertOption implements the Replace interface
259+
func (opt OptWriteConcern) ConvertOption() option.ReplaceOptioner {
260+
return option.OptWriteConcern(opt)
261+
}

0 commit comments

Comments
 (0)