diff --git a/momento/dictionary_test.go b/momento/dictionary_test.go index 1e575a44..e35d2783 100644 --- a/momento/dictionary_test.go +++ b/momento/dictionary_test.go @@ -223,6 +223,15 @@ var _ = Describe("Dictionary methods", func() { map[string]string{"myField1": "myValue1", "myField2": "myValue2"}, map[string][]byte{"myField1": []byte("myValue1"), "myField2": []byte("myValue2")}, ), + Entry( + "with empty values", + []DictionaryElement{ + {Field: Bytes("myField1"), Value: String("")}, + {Field: String("myField2"), Value: Bytes("")}, + }, + map[string]string{"myField1": "", "myField2": ""}, + map[string][]byte{"myField1": []byte(""), "myField2": []byte("")}, + ), ) It("returns an error if an item field is empty", func() { @@ -635,7 +644,6 @@ var _ = Describe("Dictionary methods", func() { }) When("removing multiple fields", func() { - It("properly removes multiple fields", func() { removeResp, err := sharedContext.Client.DictionaryRemoveFields(sharedContext.Ctx, &DictionaryRemoveFieldsRequest{ CacheName: sharedContext.CacheName, diff --git a/momento/list_test.go b/momento/list_test.go index 45937e07..14cc7c2c 100644 --- a/momento/list_test.go +++ b/momento/list_test.go @@ -203,6 +203,22 @@ var _ = Describe("List methods", func() { ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) + It("accepts an empty value", func() { + Expect( + sharedContext.Client.ListPushBack(sharedContext.Ctx, &ListPushBackRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + Value: String(""), + }), + ).To(BeAssignableToTypeOf(&ListPushBackSuccess{})) + + fetchResp, err := sharedContext.Client.ListFetch(sharedContext.Ctx, &ListFetchRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + }) + Expect(err).To(BeNil()) + Expect(fetchResp).To(HaveListLength(1)) + }) }) When("pushing to the back of the list", func() { @@ -262,6 +278,22 @@ var _ = Describe("List methods", func() { ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) + It("accepts an empty value", func() { + Expect( + sharedContext.Client.ListPushBack(sharedContext.Ctx, &ListPushBackRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + Value: String(""), + }), + ).To(BeAssignableToTypeOf(&ListPushBackSuccess{})) + + fetchResp, err := sharedContext.Client.ListFetch(sharedContext.Ctx, &ListFetchRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + }) + Expect(err).To(BeNil()) + Expect(fetchResp).To(HaveListLength(1)) + }) }) }) @@ -336,6 +368,22 @@ var _ = Describe("List methods", func() { ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) + It("accepts an empty value", func() { + Expect( + sharedContext.Client.ListConcatenateFront(sharedContext.Ctx, &ListConcatenateFrontRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + Values: []Value{String("")}, + }), + ).To(BeAssignableToTypeOf(&ListConcatenateFrontSuccess{})) + + fetchResp, err := sharedContext.Client.ListFetch(sharedContext.Ctx, &ListFetchRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + }) + Expect(err).To(BeNil()) + Expect(fetchResp).To(HaveListLength(1)) + }) }) When("concatenating to the back of the list", func() { @@ -406,8 +454,23 @@ var _ = Describe("List methods", func() { ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) - }) + It("accepts an empty value", func() { + Expect( + sharedContext.Client.ListConcatenateBack(sharedContext.Ctx, &ListConcatenateBackRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + Values: []Value{String("")}, + }), + ).To(BeAssignableToTypeOf(&ListConcatenateBackSuccess{})) + fetchResp, err := sharedContext.Client.ListFetch(sharedContext.Ctx, &ListFetchRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + }) + Expect(err).To(BeNil()) + Expect(fetchResp).To(HaveListLength(1)) + }) + }) }) Describe("list pop", func() { @@ -546,6 +609,38 @@ var _ = Describe("List methods", func() { ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) + It("accepts an empty value", func() { + Expect( + sharedContext.Client.ListConcatenateFront( + sharedContext.Ctx, + &ListConcatenateFrontRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + Values: []Value{String("one"), String(""), String("three")}, + }, + ), + ).To(BeAssignableToTypeOf(&ListConcatenateFrontSuccess{})) + + Expect( + sharedContext.Client.ListRemoveValue(sharedContext.Ctx, &ListRemoveValueRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + Value: String(""), + }), + ).To(BeAssignableToTypeOf(&ListRemoveValueSuccess{})) + + fetchResp, err := sharedContext.Client.ListFetch(sharedContext.Ctx, &ListFetchRequest{ + CacheName: sharedContext.CacheName, + ListName: sharedContext.CollectionName, + }) + Expect(err).To(BeNil()) + switch result := fetchResp.(type) { + case *ListFetchHit: + Expect( + result.ValueListString(), + ).To(Equal([]string{"one", "three"})) + } + }) }) When("removing a value that appears multiple times", func() { diff --git a/momento/requester.go b/momento/requester.go index 197a5df8..14ad3777 100644 --- a/momento/requester.go +++ b/momento/requester.go @@ -84,85 +84,54 @@ func buildError(errorCode string, errorMessage string, originalError error) Mome func prepareName(name string, label string) (string, error) { if len(strings.TrimSpace(name)) < 1 { - errStr := fmt.Sprintf("%v cannot be empty", label) + errStr := fmt.Sprintf("%v cannot be empty or blank", label) return "", buildError(momentoerrors.InvalidArgumentError, errStr, nil) } return name, nil } -func prepareElementValue(value Value) ([]byte, error) { - if value == nil { - return nil, buildError( - momentoerrors.InvalidArgumentError, "element value cannot be nil", nil, - ) - } - - // just validate not empty using prepareName - _, err := prepareName(value.asString(), "element value") - if err != nil { - return nil, err - } - - return value.asBytes(), nil -} - func prepareCacheName(r hasCacheName) (string, error) { return prepareName(r.cacheName(), "Cache name") } func prepareKey(r hasKey) ([]byte, error) { - if r.key() == nil { - return nil, buildError(momentoerrors.InvalidArgumentError, "key cannot be nil or empty", nil) + if err := validateNotEmpty(r.key(), "key"); err != nil { + return nil, err } - key := r.key().asBytes() - if len(key) == 0 { - return nil, buildError(momentoerrors.InvalidArgumentError, "key cannot be nil or empty", nil) - } - return key, nil + return r.key().asBytes(), nil } func prepareField(r hasField) ([]byte, error) { - if r.field() == nil { - return nil, buildError( - momentoerrors.InvalidArgumentError, "field cannot be nil or empty", nil, - ) - } - field := r.field().asBytes() - if err := validateNotEmpty(field, "field"); err != nil { + if err := validateNotEmpty(r.field(), "field"); err != nil { return nil, err } - return field, nil + return r.field().asBytes(), nil } func prepareFields(r hasFields) ([][]byte, error) { if r.fields() == nil { - return nil, buildError(momentoerrors.InvalidArgumentError, "fields cannot be nil or empty", nil) + return nil, buildError(InvalidArgumentError, "fields cannot be nil", nil) } + var fields [][]byte - for _, valueField := range r.fields() { - if valueField == nil { - return nil, buildError(momentoerrors.InvalidArgumentError, "fields cannot be nil or empty", nil) - } - field := valueField.asBytes() + for _, field := range r.fields() { if err := validateNotEmpty(field, "field"); err != nil { - return nil, buildError(momentoerrors.InvalidArgumentError, "fields cannot be nil or empty", nil) + return nil, err } - fields = append(fields, field) + fields = append(fields, field.asBytes()) } return fields, nil } -func prepareValue(r hasValue) ([]byte, momentoerrors.MomentoSvcErr) { - if r.value() == nil { - return []byte{}, buildError( - momentoerrors.InvalidArgumentError, "value may not be nil", nil, - ) +func prepareValue(r hasValue) ([]byte, error) { + if err := validateNotNil(r.value(), "value"); err != nil { + return []byte{}, err } return r.value().asBytes(), nil } -func prepareValues(r hasValues) ([][]byte, momentoerrors.MomentoSvcErr) { +func prepareValues(r hasValues) ([][]byte, error) { values, err := momentoValuesToPrimitiveByteList(r.values()) if err != nil { return [][]byte{}, err @@ -172,12 +141,10 @@ func prepareValues(r hasValues) ([][]byte, momentoerrors.MomentoSvcErr) { func prepareDictionaryElements(r hasDictionaryElements) ([]DictionaryElement, error) { for _, v := range r.dictionaryElements() { - if v.Value == nil || v.Field == nil { - return nil, buildError( - momentoerrors.InvalidArgumentError, "element fields and values may not be nil", nil, - ) + if err := validateNotNil(v.Value, "value"); err != nil { + return nil, err } - if err := validateNotEmpty(v.Field.asBytes(), "element field"); err != nil { + if err := validateNotEmpty(v.Field, "element field"); err != nil { return nil, err } } @@ -210,29 +177,44 @@ func prepareTtl(r hasTtl, defaultTtl time.Duration) (uint64, error) { return uint64(ttl.Milliseconds()), nil } -func momentoValuesToPrimitiveByteList(i []Value) ([][]byte, momentoerrors.MomentoSvcErr) { - if i == nil { - return [][]byte{}, buildError(momentoerrors.InvalidArgumentError, "values may not be nil", nil) +func momentoValuesToPrimitiveByteList(values []Value) ([][]byte, error) { + if values == nil { + return nil, buildError(momentoerrors.InvalidArgumentError, "values cannot be nil", nil) } + var rList [][]byte - for _, mb := range i { - if mb == nil { - return [][]byte{}, buildError(momentoerrors.InvalidArgumentError, "values may not be nil", nil) + for _, mb := range values { + if err := validateNotNil(mb, "value"); err != nil { + return [][]byte{}, err } rList = append(rList, mb.asBytes()) } return rList, nil } -func validateNotEmpty(field []byte, label string) error { - if len(field) == 0 { +func validateNotEmpty(thing Value, label string) error { + if err := validateNotNil(thing, label); err != nil { + return err + } + + if len(thing.asBytes()) == 0 { return buildError( - momentoerrors.InvalidArgumentError, fmt.Sprintf("%s cannot be empty", label), nil, + momentoerrors.InvalidArgumentError, fmt.Sprintf("%v cannot be empty", label), nil, ) } return nil } +func validateNotNil(value Value, label string) error { + if value == nil { + return buildError( + momentoerrors.InvalidArgumentError, fmt.Sprintf("%v cannot be nil", label), nil, + ) + } + + return nil +} + func ElementsFromMapStringString(theMap map[string]string) []DictionaryElement { var elements []DictionaryElement for k, v := range theMap { diff --git a/momento/sorted_set_get_rank.go b/momento/sorted_set_get_rank.go index 51a19b78..def00ee7 100644 --- a/momento/sorted_set_get_rank.go +++ b/momento/sorted_set_get_rank.go @@ -22,6 +22,8 @@ func (r *SortedSetGetRankRequest) cacheName() string { return r.CacheName } func (r *SortedSetGetRankRequest) requestName() string { return "Sorted set get rank" } +func (r *SortedSetGetRankRequest) value() Value { return r.Value } + func (r *SortedSetGetRankRequest) initGrpcRequest(scsDataClient) error { var err error @@ -30,7 +32,7 @@ func (r *SortedSetGetRankRequest) initGrpcRequest(scsDataClient) error { } var value []byte - if value, err = prepareElementValue(r.Value); err != nil { + if value, err = prepareValue(r); err != nil { return err } diff --git a/momento/sorted_set_increment_score.go b/momento/sorted_set_increment_score.go index dd71d7af..e125c57f 100644 --- a/momento/sorted_set_increment_score.go +++ b/momento/sorted_set_increment_score.go @@ -29,6 +29,8 @@ func (r *SortedSetIncrementScoreRequest) cacheName() string { return r.CacheName func (r *SortedSetIncrementScoreRequest) requestName() string { return "Sorted set increment" } +func (r *SortedSetIncrementScoreRequest) value() Value { return r.Value } + func (r *SortedSetIncrementScoreRequest) ttl() time.Duration { return r.Ttl.Ttl } func (r *SortedSetIncrementScoreRequest) collectionTtl() *utils.CollectionTtl { return r.Ttl } @@ -47,7 +49,7 @@ func (r *SortedSetIncrementScoreRequest) initGrpcRequest(client scsDataClient) e } var value []byte - if value, err = prepareElementValue(r.Value); err != nil { + if value, err = prepareValue(r); err != nil { return err } diff --git a/momento/sorted_set_test.go b/momento/sorted_set_test.go index 1b7000b1..56586dcd 100644 --- a/momento/sorted_set_test.go +++ b/momento/sorted_set_test.go @@ -482,6 +482,23 @@ var _ = Describe("SortedSet", func() { ), ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) + + It(`accepts an empty value`, func() { + putElements([]*SortedSetPutElement{ + {Value: String(""), Score: 0}, + }) + + Expect( + sharedContext.Client.SortedSetGetRank( + sharedContext.Ctx, + &SortedSetGetRankRequest{ + CacheName: sharedContext.CacheName, + SetName: sharedContext.CollectionName, + Value: String(""), + }, + ), + ).To(Equal(SortedSetGetRankHit(0))) + }) }) Describe(`SortedSetGetScores`, func() { @@ -651,6 +668,23 @@ var _ = Describe("SortedSet", func() { ).Error().To(HaveMomentoErrorCode(InvalidArgumentError)) }) + It(`accepts an empty value`, func() { + putElements([]*SortedSetPutElement{ + {Value: String(""), Score: 50}, + }) + + resp, err := sharedContext.Client.SortedSetIncrementScore( + sharedContext.Ctx, + &SortedSetIncrementScoreRequest{ + CacheName: sharedContext.CacheName, + SetName: sharedContext.CollectionName, + Value: String(""), + Amount: 10, + }, + ) + Expect(err).To(BeNil()) + Expect(resp).To(BeAssignableToTypeOf(SortedSetIncrementScoreSuccess(60))) + }) }) Describe(`SortedSetRemove`, func() {