Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions resp/resp3/flatten.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ func cleanFloatStr(str string) string {
return str
}

// Source: https://cs.opensource.google/go/go/+/refs/tags/go1.19.2:src/encoding/json/encode.go;drc=d0b0b10b5cbb28d53403c2bd6af343581327e946;l=339
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false
}

// Flatten accepts any type accepted by Marshal, except a resp.Marshaler, and
// converts it into a flattened array of strings. For example:
//
Expand All @@ -33,7 +52,6 @@ func cleanFloatStr(str string) string {
// Flatten([]string{"a","b"}) -> {"a", "b"}
// Flatten(map[string]int{"a":5,"b":10}) -> {"a","5","b","10"}
// Flatten([]map[int]float64{{1:2, 3:4},{5:6},{}}) -> {"1","2","3","4","5","6"})
//
func Flatten(i interface{}, o *resp.Opts) ([]string, error) {
f := flattener{
opts: o,
Expand Down Expand Up @@ -165,7 +183,7 @@ func (f *flattener) flatten(i interface{}) error {
l := vv.NumField()
for i := 0; i < l; i++ {
ft, fv := tt.Field(i), vv.Field(i)
tag := ft.Tag.Get("redis")
tag, tagOpts := parseTag(ft.Tag.Get("redis"))
if ft.Anonymous {
if fv = reflect.Indirect(fv); !fv.IsValid() { // fv is nil
continue
Expand All @@ -177,12 +195,22 @@ func (f *flattener) flatten(i interface{}) error {
continue // unexported
}

isEmpty := isEmptyValue(fv)
if isEmpty && tagOpts.Contains("omitempty") {
continue
}

keyName := ft.Name
if tag != "" {
keyName = tag
}
_ = f.emit(keyName)

if isEmpty {
// Return "", setting empty value
return f.emit("")
}

if err := f.flatten(fv.Interface()); err != nil {
return err
}
Expand Down
41 changes: 41 additions & 0 deletions resp/resp3/flatten_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package resp3

import (
"testing"

"github.com/mediocregopher/radix/v4/resp"
"github.com/stretchr/testify/suite"
)

type FlattenTestSuite struct {
suite.Suite
}

// Test whether by default, an empty slice in a hashmap should be flattened to an empty value.
func (s *FlattenTestSuite) TestEmptyNestedSlice() {
testInst := struct {
Nested []string
}{}

flat, err := Flatten(testInst, resp.NewOpts())
s.NoError(err)

s.Equal([]string{"Nested", ""}, flat)
}

// Test with omitempty; empty slices should be left off altogether.
func (s *FlattenTestSuite) TestOmitEmptyNestedSlice() {
testInst := struct {
Other string // We need at least one non-empty value for commands like HMSET.
NestedOmitEmpty []string `redis:",omitempty"`
}{}

flat, err := Flatten(testInst, resp.NewOpts())
s.NoError(err)

s.Equal([]string{"Other", ""}, flat)
}

func TestFlattenTestSuite(t *testing.T) {
suite.Run(t, new(FlattenTestSuite))
}
2 changes: 1 addition & 1 deletion resp/resp3/resp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2255,7 +2255,7 @@ func getStructFields(t reflect.Type) map[string]structField {
}

key, fromTag := ft.Name, false
if tag := ft.Tag.Get("redis"); tag != "" && tag != "-" {
if tag, _ := parseTag(ft.Tag.Get("redis")); tag != "" && tag != "-" {
key, fromTag = tag, true
}
if m[key].fromTag {
Expand Down
38 changes: 38 additions & 0 deletions resp/resp3/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package resp3

import (
"strings"
)

// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string

// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt)
}

// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var name string
name, s, _ = strings.Cut(s, ",")
if name == optionName {
return true
}
}
return false
}
28 changes: 28 additions & 0 deletions resp/resp3/tags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package resp3

import (
"testing"
)

func TestTagParsing(t *testing.T) {
name, opts := parseTag("field,foobar,foo")
if name != "field" {
t.Fatalf("name = %q, want field", name)
}
for _, tt := range []struct {
opt string
want bool
}{
{"foobar", true},
{"foo", true},
{"bar", false},
} {
if opts.Contains(tt.opt) != tt.want {
t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
}
}
}