Skip to content
Merged
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
36 changes: 27 additions & 9 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ type field struct {
dereferenceType reflect.Type
}

func newField(structField reflect.StructField, structPath string, parent *field) *field {
return &field{
StructField: structField,
structPath: structPath,
parent: parent,
dereferenceType: dereferenceType(structField.Type),
}
}

func dereferenceType(typeOf reflect.Type) reflect.Type {
for typeOf.Kind() == reflect.Ptr {
typeOf = typeOf.Elem()
}
return typeOf
}

func (f *field) GetName() string {
return f.Name
}
Expand Down Expand Up @@ -50,6 +66,10 @@ func (f *field) GetStructPath() string {
}

func (f *field) GetParent() Field {
if f.parent == nil {
// nil pointer in the interface is not equal to nil
return nil
}
return f.parent
}

Expand Down Expand Up @@ -173,7 +193,12 @@ func (f *field) getPtr(obj interface{}) unsafe.Pointer {

func setPtrValue[T any](ptr unsafe.Pointer, val any) {
valSet := (*T)(ptr)
*valSet = val.(T)
if typed, ok := val.(T); ok {
*valSet = typed
return
}
// value of the named type: time.Duration for int64 field kind, *time.Duration for *int64 and so on
*valSet = reflect.ValueOf(val).Convert(reflect.TypeOf(*valSet)).Interface().(T)
}

func getPtrValue[T any](ptr unsafe.Pointer) T {
Expand Down Expand Up @@ -273,14 +298,7 @@ func (f *field) Set(obj interface{}, val interface{}) {
}

func (f *field) GetDereferencedType() reflect.Type {
if f.dereferenceType != nil {
return f.dereferenceType
}
indirectType := f.Type
for indirectType.Kind() == reflect.Ptr {
indirectType = indirectType.Elem()
}
f.dereferenceType = indirectType
// calculated once in newField, the field is read only after that and can be used from many goroutines
return f.dereferenceType
}

Expand Down
96 changes: 76 additions & 20 deletions field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,24 +381,18 @@ func TestField_GetDereferenced(t *testing.T) {
expected bool
}{
{
name: "IsDereferencedPtr",
field: &field{
StructField: rtype.Field(0),
},
name: "IsDereferencedPtr",
field: newField(rtype.Field(0), "", nil),
expected: true,
},
{
name: "AlreadyDereferencedType",
field: &field{
StructField: rtype.Field(1),
},
name: "AlreadyDereferencedType",
field: newField(rtype.Field(1), "", nil),
expected: true,
},
{
name: "NilDereferenceNotOk",
field: &field{
StructField: rtype.Field(2),
},
name: "NilDereferenceNotOk",
field: newField(rtype.Field(2), "", nil),
expected: false,
},
}
Expand All @@ -417,9 +411,9 @@ func TestField_GetDereferencedType(t *testing.T) {
type someStruct struct {
MyField int
}
dereferencedTypeIntPtrField := &field{StructField: reflect.StructField{
dereferencedTypeIntPtrField := newField(reflect.StructField{
Type: reflect.TypeOf(new(int)),
}}
}, "", nil)

testCases := []struct {
name string
Expand All @@ -428,9 +422,9 @@ func TestField_GetDereferencedType(t *testing.T) {
}{
{
name: "DereferencedTypeInt",
field: &field{StructField: reflect.StructField{
field: newField(reflect.StructField{
Type: reflect.TypeOf(0),
}},
}, "", nil),
expected: reflect.TypeOf(0),
},
{
Expand All @@ -445,16 +439,16 @@ func TestField_GetDereferencedType(t *testing.T) {
},
{
name: "DereferencedTypeStruct",
field: &field{StructField: reflect.StructField{
field: newField(reflect.StructField{
Type: reflect.TypeOf(someStruct{}),
}},
}, "", nil),
expected: reflect.TypeOf(someStruct{}),
},
{
name: "DereferencedTypeStructPtr",
field: &field{StructField: reflect.StructField{
field: newField(reflect.StructField{
Type: reflect.TypeOf(new(someStruct)),
}},
}, "", nil),
expected: reflect.TypeOf(someStruct{}),
},
}
Expand Down Expand Up @@ -483,3 +477,65 @@ func TestField_ReflectStructField(t *testing.T) {
fld.GetOffset()
fld.GetPkgPath()
}

func TestField_GetParent(t *testing.T) {
type Config struct {
Name string
Nested struct {
Inner struct {
Value int
}
}
}
storage, err := Get[Config]()
assert.NoError(t, err)

// field without parent returns nil interface, not nil pointer in the interface
assert.True(t, storage.MustFind("Name").GetParent() == nil)
assert.True(t, storage.MustFind("Nested").GetParent() == nil)

parent := storage.MustFind("Nested.Inner.Value").GetParent()
assert.Equal(t, "Nested.Inner", parent.GetStructPath())
assert.Equal(t, "Nested", parent.GetParent().GetStructPath())
assert.True(t, parent.GetParent().GetParent() == nil)
}

type namedString string
type namedInt int
type namedBool bool

func TestField_SetNamedTypes(t *testing.T) {
type Config struct {
Duration time.Duration
DurationPtr *time.Duration
String namedString
Int namedInt
IntPtr *namedInt
Bool namedBool
Plain int64
}
storage, err := Get[Config]()
assert.NoError(t, err)

duration, number := 5*time.Second, namedInt(42)
conf := &Config{}
storage.MustFind("Duration").Set(conf, duration)
storage.MustFind("DurationPtr").Set(conf, &duration)
storage.MustFind("String").Set(conf, namedString("named"))
storage.MustFind("Int").Set(conf, number)
storage.MustFind("IntPtr").Set(conf, &number)
storage.MustFind("Bool").Set(conf, namedBool(true))
// value of the base type is still supported
storage.MustFind("Plain").Set(conf, int64(7))

assert.Equal(t, Config{
Duration: duration,
DurationPtr: &duration,
String: "named",
Int: 42,
IntPtr: &number,
Bool: true,
Plain: 7,
}, *conf)
assert.Same(t, &duration, conf.DurationPtr)
}
38 changes: 25 additions & 13 deletions storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"errors"
"fmt"
"reflect"
"sync"
"unsafe"
)

var cache = map[reflect.Type]Storage{}
// cache - storages by the struct type, reflect.Type -> Storage
var cache sync.Map

// Get returns a map of field objects.
// It takes a parameter `T` of type `any`, representing the type to be used for Fields map creation.
Expand Down Expand Up @@ -50,19 +52,21 @@ func getFrom(typeOf reflect.Type) (Storage, error) {
(typeOf.Kind() == reflect.Pointer && typeOf.Elem().Kind() != reflect.Struct) {
return nil, fmt.Errorf("not supported type: %v, only struct and ptr to struct is supported", typeOf)
}
if tFields, ok := cache[typeOf]; ok {
return tFields, nil
if cached, ok := cache.Load(typeOf); ok {
return cached.(Storage), nil
}
tFields := map[string]Field{}
count := new(int)
calculateFields(typeOf, count)
slice := make([]string, 0, *count)
getFieldsMapRecursive(typeOf, "", &tFields, &slice, 0)
cache[typeOf] = &storage{

// storage can be already created by another goroutine, in this case the stored one is used
cached, _ := cache.LoadOrStore(typeOf, &storage{
asMap: tFields,
paths: slice,
}
return cache[typeOf], nil
})
return cached.(Storage), nil
}

func calculateFields(confTypeOf reflect.Type, count *int) {
Expand Down Expand Up @@ -93,12 +97,14 @@ func getFieldsMapRecursive(confTypeOf reflect.Type, path string, f *map[string]F
}
switch fieldTypeOf.Type.Kind() {
case reflect.Struct:
fld := &field{StructField: fieldTypeOf, structPath: path + fieldTypeOf.Name, parent: parent}
fld := newField(fieldTypeOf, path+fieldTypeOf.Name, parent)
// offset from the root struct, not from the parent
fld.Offset = fld.Offset + offset
(*f)[path+fieldTypeOf.Name] = fld
*s = append(*s, fld.structPath)
getFieldsMapRecursive(fieldTypeOf.Type, path+fieldTypeOf.Name, f, s, offset+fieldTypeOf.Offset)
getFieldsMapRecursive(fieldTypeOf.Type, path+fieldTypeOf.Name, f, s, fld.Offset)
default:
fld := &field{StructField: fieldTypeOf, structPath: path + fieldTypeOf.Name, parent: parent}
fld := newField(fieldTypeOf, path+fieldTypeOf.Name, parent)
fld.Offset = fld.Offset + offset
*s = append(*s, fld.structPath)
(*f)[path+fieldTypeOf.Name] = fld
Expand Down Expand Up @@ -126,19 +132,25 @@ func (s *storage) GetFieldByPtr(structPtr, fieldPtr any) (Field, error) {
sPtr := ((*[2]unsafe.Pointer)(unsafe.Pointer(&structPtr)))[1]
offset := uintptr(fPtr) - uintptr(sPtr)

// struct and its first field has the same offset, so the field with the same type is preferred
var matchKind Field
for _, path := range s.GetAllPaths() {
fld := s.MustFind(path)

if fld.GetOffset() != offset {
continue
}

matchType := fld.GetType().Kind() == fldType.Kind()
if !matchType {
continue
if fld.GetType() == fldType {
return fld, nil
}

return fld, nil
if matchKind == nil && fld.GetType().Kind() == fldType.Kind() {
matchKind = fld
}
}
if matchKind != nil {
return matchKind, nil
}

return nil, errors.New("field not found")
Expand Down
Loading
Loading