Skip to content
Closed
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
69 changes: 57 additions & 12 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,43 @@ func (c *cache) registerConverter(value interface{}, converterFunc Converter) {
c.regconv[reflect.TypeOf(value)] = converterFunc
}

// pathIter is used to iterate over the dotted notation argument to (*cache).parsePath.
// internally it utilizes strings.Cut to minimize allocations and garbage generated.
//
// this should be used with a for loop as:
//
// var it pathIter
// for key, ok := it.start(path); ok; key, ok = it.advance() {}
type pathIter struct {
rest string
lastFound bool
}

// start initializes the iteration, and returns the first key.
func (p *pathIter) start(path string) (string, bool) {
// init the iter for the first call to next, so that the first call will either
// cut on "." and return the "before", or it will return path if it does not contain
// ".".
p.rest = path
p.lastFound = true
return p.next()
}

// advance advances the iteration to the next key.
func (p *pathIter) advance() (string, bool) {
return p.next()
}

// next should only be used by (*pathIter).start and (*pathIter).advance. it will cut p.rest,
// and return the "before", storing the "after" for the subsequent call.
func (p *pathIter) next() (key string, ok bool) {
var found bool
key, p.rest, found = strings.Cut(p.rest, ".")
ok = found || p.lastFound
p.lastFound = found
return key, ok
}

// parsePath parses a path in dotted notation verifying that it is a valid
// path to a struct field.
//
Expand All @@ -48,41 +85,48 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
var field *fieldInfo
var index64 int64
var err error
parts := make([]pathPart, 0)
path := make([]string, 0)
keys := strings.Split(p, ".")
for i := 0; i < len(keys); i++ {
// pre-allocate parts and path using the number of ".".
// path will be appended to every valid element of the path, but parts will
// only be appended to for each slice of struct.
n := strings.Count(p, ".")
if n == 0 { // note: go1.21 max()
n = 1
}
parts := make([]pathPart, 0, n)
path := make([]string, 0, n)
var it pathIter
for key, ok := it.start(p); ok; key, ok = it.advance() {
if t.Kind() != reflect.Struct {
return nil, errInvalidPath
}
if struc = c.get(t); struc == nil {
return nil, errInvalidPath
}
if field = struc.get(keys[i]); field == nil {
if field = struc.get(key); field == nil {
return nil, errInvalidPath
}
// Valid field. Append index.
path = append(path, field.name)
if field.isSliceOfStructs && (!field.unmarshalerInfo.IsValid || (field.unmarshalerInfo.IsValid && field.unmarshalerInfo.IsSliceElement)) {
// Parse a special case: slices of structs.
// i+1 must be the slice index.
// next key must be the slice index.
//
// Now that struct can implements TextUnmarshaler interface,
// we don't need to force the struct's fields to appear in the path.
// So checking i+2 is not necessary anymore.
i++
if i+1 > len(keys) {
key, ok = it.advance()
if !ok {
return nil, errInvalidPath
}
if index64, err = strconv.ParseInt(keys[i], 10, 0); err != nil {
if index64, err = strconv.ParseInt(key, 10, 0); err != nil {
return nil, errInvalidPath
}
parts = append(parts, pathPart{
path: path,
path: path[:len(path):len(path)], // note: go1.21 slices.Clip()
field: field,
index: int(index64),
})
path = make([]string, 0)
path = path[len(path):]

// Get the next struct type, dropping ptrs.
if field.typ.Kind() == reflect.Ptr {
Expand All @@ -104,10 +148,11 @@ func (c *cache) parsePath(p string, t reflect.Type) ([]pathPart, error) {
}
// Add the remaining.
parts = append(parts, pathPart{
path: path,
path: path[:len(path):len(path)], // note: go1.21 slices.Clip()
field: field,
index: -1,
})
parts = parts[:len(parts):len(parts)] // note: go1.21 slices.Clip()
return parts, nil
}

Expand Down
65 changes: 65 additions & 0 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2527,3 +2527,68 @@ func TestDecoder_SetMaxSize(t *testing.T) {
}
})
}

type keyvalue struct {
Key string
Value string
}

type sliceOfStructInput struct {
Attributes []keyvalue
Tags []keyvalue
}

func BenchmarkSliceOfStruct(b *testing.B) {
d := NewDecoder()
v := map[string][]string{
"Attributes.0.Key": {"foo0"},
"Attributes.0.Value": {"bar0"},
"Attributes.1.Key": {"foo1"},
"Attributes.1.Value": {"bar1"},
"Attributes.2.Key": {"foo2"},
"Attributes.2.Value": {"bar2"},
"Attributes.3.Key": {"foo3"},
"Attributes.3.Value": {"bar3"},
"Attributes.4.Key": {"foo4"},
"Attributes.4.Value": {"bar4"},
"Attributes.5.Key": {"foo5"},
"Attributes.5.Value": {"bar5"},
"Tags.0.Key": {"baz0"},
"Tags.0.Value": {"bam0"},
"Tags.1.Key": {"baz1"},
"Tags.1.Value": {"bam1"},
"Tags.2.Key": {"baz2"},
"Tags.2.Value": {"bam2"},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
in := &sliceOfStructInput{}
_ = d.Decode(in, v)
}
}

func TestPathIter(t *testing.T) {
testcases := []string{
"a",
"a.b",
".b.",
".",
"..",
"",
}
for _, tc := range testcases {
t.Run(tc, func(t *testing.T) {
var (
it pathIter
split = strings.Split(tc, ".")
collect = make([]string, 0, len(split))
)
for k, ok := it.start(tc); ok; k, ok = it.advance() {
collect = append(collect, k)
}
if !reflect.DeepEqual(split, collect) {
t.Fatalf("expected: %q | got: %q", split, collect)
}
})
}
}