diff --git a/README.md b/README.md index 1a4a2fd..e76542c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# Archived project. No maintenance. +# Fork reason + +Support for embedded anonymous fields without "flatten" tag. + +- SetValuesFromMap function added. + +# Archived project. No maintenance. This project is not maintained anymore and is archived. Feel free to fork and make your own changes if needed. For more detail read my blog post: [Taking an indefinite sabbatical from my projects](https://arslan.io/2018/10/09/taking-an-indefinite-sabbatical-from-my-projects/) diff --git a/field.go b/field.go index e697832..23270e0 100644 --- a/field.go +++ b/field.go @@ -95,8 +95,8 @@ func (f *Field) Zero() error { // of a nested struct . A struct tag with the content of "-" ignores the // checking of that particular field. Example: // -// // Field is ignored by this package. -// Field *http.Request `structs:"-"` +// // Field is ignored by this package. +// Field *http.Request `structs:"-"` // // It panics if field is not exported or if field's kind is not struct func (f *Field) Fields() []*Field { diff --git a/field_test.go b/field_test.go index ed14dcc..9e02baf 100644 --- a/field_test.go +++ b/field_test.go @@ -29,7 +29,7 @@ type Bar struct { g []string } -func newStruct() *Struct { +func newStruct() *Struct[*Foo] { b := &Bar{ E: "example", F: 2, diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ff7afb8 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/jadegopher/structs + +go 1.22.0 diff --git a/structs.go b/structs.go index 3a87706..aaf1bef 100644 --- a/structs.go +++ b/structs.go @@ -3,7 +3,6 @@ package structs import ( "fmt" - "reflect" ) @@ -16,20 +15,23 @@ var ( // Struct encapsulates a struct type to provide several high level functions // around the struct. -type Struct struct { - raw interface{} +type Struct[T any] struct { + raw T value reflect.Value TagName string } // New returns a new *Struct with the struct s. It panics if the s's kind is // not struct. -func New(s interface{}) *Struct { - return &Struct{ +func New[T any](s T) *Struct[T] { + converted := strctVal(s) + result := &Struct[T]{ raw: s, - value: strctVal(s), + value: converted, TagName: DefaultTagName, } + + return result } // Map converts the given struct to a map[string]interface{}, where the keys @@ -38,55 +40,112 @@ func New(s interface{}) *Struct { // can be changed in the struct field's tag value. The "structs" key in the // struct's field tag value is the key name. Example: // -// // Field appears in map as key "myName". -// Name string `structs:"myName"` +// // Field appears in map as key "myName". +// Name string `structs:"myName"` // // A tag value with the content of "-" ignores that particular field. Example: // -// // Field is ignored by this package. -// Field bool `structs:"-"` +// // Field is ignored by this package. +// Field bool `structs:"-"` // // A tag value with the content of "string" uses the stringer to get the value. Example: // -// // The value will be output of Animal's String() func. -// // Map will panic if Animal does not implement String(). -// Field *Animal `structs:"field,string"` +// // The value will be output of Animal's String() func. +// // Map will panic if Animal does not implement String(). +// Field *Animal `structs:"field,string"` // // A tag value with the option of "flatten" used in a struct field is to flatten its fields // in the output map. Example: // -// // The FieldStruct's fields will be flattened into the output map. -// FieldStruct time.Time `structs:",flatten"` +// // The FieldStruct's fields will be flattened into the output map. +// FieldStruct time.Time `structs:",flatten"` // // A tag value with the option of "omitnested" stops iterating further if the type // is a struct. Example: // -// // Field is not processed further by this package. -// Field time.Time `structs:"myName,omitnested"` -// Field *http.Request `structs:",omitnested"` +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` // // A tag value with the option of "omitempty" ignores that particular field if // the field value is empty. Example: // -// // Field appears in map as key "myName", but the field is -// // skipped if empty. -// Field string `structs:"myName,omitempty"` +// // Field appears in map as key "myName", but the field is +// // skipped if empty. +// Field string `structs:"myName,omitempty"` // -// // Field appears in map as key "Field" (the default), but -// // the field is skipped if empty. -// Field string `structs:",omitempty"` +// // Field appears in map as key "Field" (the default), but +// // the field is skipped if empty. +// Field string `structs:",omitempty"` // // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. -func (s *Struct) Map() map[string]interface{} { +func (s *Struct[T]) Map() map[string]interface{} { out := make(map[string]interface{}) s.FillMap(out) return out } +func setValueFromMapRec(value reflect.Value, newValues map[string]interface{}, tagName string) { + if newValues == nil { + return + } + + for _, field := range structFields(value, tagName) { + name := field.Name + val := value.FieldByName(name) + + tag, _ := parseTag(field.Tag.Get(tagName)) + if tag != "" { + name = tag + } + + // special case for anonymous fields + if field.Anonymous && val.Kind() == reflect.Struct { + setValueFromMapRec(val, newValues, tagName) + } + + if newValue, ok := newValues[name]; ok && val.CanSet() { + if m, ok := newValue.(map[string]interface{}); ok && val.Kind() == reflect.Struct { + setValueFromMapRec(val, m, tagName) + } + + reflectedNewValue := reflect.ValueOf(newValue) + if val.Type() != reflectedNewValue.Type() { + continue + } + + val.Set(reflectedNewValue) + } + } +} + +// SetValuesFromMap allows to apply map values to struct. Map must have similar structure. +// Provided struct must have a pointer type. +func (s *Struct[T]) SetValuesFromMap(values map[string]interface{}) { + if values == nil { + return + } + + setValueFromMapRec(s.value, values, s.TagName) + + // type of s.value can be changed, so we need to retrieve original type + original := reflect.ValueOf(s.raw) + original.Elem().Set(s.value) + + val, ok := original.Interface().(T) + if ok { + s.raw = val + } +} + +func (s *Struct[T]) Raw() T { + return s.raw +} + // FillMap is the same as Map. Instead of returning the output, it fills the // given map. -func (s *Struct) FillMap(out map[string]interface{}) { +func (s *Struct[T]) FillMap(out map[string]interface{}) { if out == nil { return } @@ -139,7 +198,8 @@ func (s *Struct) FillMap(out map[string]interface{}) { continue } - if isSubStruct && (tagOpts.Has("flatten")) { + // Fields in struct can be anonymous, and we can't ignore such fields even if tag "flatten not specified". + if isSubStruct && (tagOpts.Has("flatten")) || isSubStruct && field.Anonymous { for k := range finalVal.(map[string]interface{}) { out[k] = finalVal.(map[string]interface{})[k] } @@ -153,25 +213,25 @@ func (s *Struct) FillMap(out map[string]interface{}) { // struct tag with the content of "-" ignores the that particular field. // Example: // -// // Field is ignored by this package. -// Field int `structs:"-"` +// // Field is ignored by this package. +// Field int `structs:"-"` // // A value with the option of "omitnested" stops iterating further if the type // is a struct. Example: // -// // Fields is not processed further by this package. -// Field time.Time `structs:",omitnested"` -// Field *http.Request `structs:",omitnested"` +// // Fields is not processed further by this package. +// Field time.Time `structs:",omitnested"` +// Field *http.Request `structs:",omitnested"` // // A tag value with the option of "omitempty" ignores that particular field and // is not added to the values if the field value is empty. Example: // -// // Field is skipped if empty -// Field string `structs:",omitempty"` +// // Field is skipped if empty +// Field string `structs:",omitempty"` // // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. -func (s *Struct) Values() []interface{} { +func (s *Struct[T]) Values() []interface{} { fields := s.structFields() var t []interface{} @@ -215,22 +275,22 @@ func (s *Struct) Values() []interface{} { // Fields returns a slice of Fields. A struct tag with the content of "-" // ignores the checking of that particular field. Example: // -// // Field is ignored by this package. -// Field bool `structs:"-"` +// // Field is ignored by this package. +// Field bool `structs:"-"` // // It panics if s's kind is not struct. -func (s *Struct) Fields() []*Field { +func (s *Struct[T]) Fields() []*Field { return getFields(s.value, s.TagName) } // Names returns a slice of field names. A struct tag with the content of "-" // ignores the checking of that particular field. Example: // -// // Field is ignored by this package. -// Field bool `structs:"-"` +// // Field is ignored by this package. +// Field bool `structs:"-"` // // It panics if s's kind is not struct. -func (s *Struct) Names() []string { +func (s *Struct[T]) Names() []string { fields := getFields(s.value, s.TagName) names := make([]string, len(fields)) @@ -272,7 +332,7 @@ func getFields(v reflect.Value, tagName string) []*Field { // Field returns a new Field struct that provides several high level functions // around a single struct field entity. It panics if the field is not found. -func (s *Struct) Field(name string) *Field { +func (s *Struct[T]) Field(name string) *Field { f, ok := s.FieldOk(name) if !ok { panic("field not found") @@ -284,7 +344,7 @@ func (s *Struct) Field(name string) *Field { // FieldOk returns a new Field struct that provides several high level functions // around a single struct field entity. The boolean returns true if the field // was found. -func (s *Struct) FieldOk(name string) (*Field, bool) { +func (s *Struct[T]) FieldOk(name string) (*Field, bool) { t := s.value.Type() field, ok := t.FieldByName(name) @@ -303,19 +363,19 @@ func (s *Struct) FieldOk(name string) (*Field, bool) { // initialized) A struct tag with the content of "-" ignores the checking of // that particular field. Example: // -// // Field is ignored by this package. -// Field bool `structs:"-"` +// // Field is ignored by this package. +// Field bool `structs:"-"` // // A value with the option of "omitnested" stops iterating further if the type // is a struct. Example: // -// // Field is not processed further by this package. -// Field time.Time `structs:"myName,omitnested"` -// Field *http.Request `structs:",omitnested"` +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` // // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. -func (s *Struct) IsZero() bool { +func (s *Struct[T]) IsZero() bool { fields := s.structFields() for _, field := range fields { @@ -350,19 +410,19 @@ func (s *Struct) IsZero() bool { // A struct tag with the content of "-" ignores the checking of that particular // field. Example: // -// // Field is ignored by this package. -// Field bool `structs:"-"` +// // Field is ignored by this package. +// Field bool `structs:"-"` // // A value with the option of "omitnested" stops iterating further if the type // is a struct. Example: // -// // Field is not processed further by this package. -// Field time.Time `structs:"myName,omitnested"` -// Field *http.Request `structs:",omitnested"` +// // Field is not processed further by this package. +// Field time.Time `structs:"myName,omitnested"` +// Field *http.Request `structs:",omitnested"` // // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. -func (s *Struct) HasZero() bool { +func (s *Struct[T]) HasZero() bool { fields := s.structFields() for _, field := range fields { @@ -395,14 +455,14 @@ func (s *Struct) HasZero() bool { // Name returns the structs's type name within its package. For more info refer // to Name() function. -func (s *Struct) Name() string { +func (s *Struct[T]) Name() string { return s.value.Type().Name() } // structFields returns the exported struct fields for a given s struct. This // is a convenient helper method to avoid duplicate code in some of the // functions. -func (s *Struct) structFields() []reflect.StructField { +func (s *Struct[T]) structFields() []reflect.StructField { t := s.value.Type() var f []reflect.StructField @@ -425,10 +485,33 @@ func (s *Struct) structFields() []reflect.StructField { return f } -func strctVal(s interface{}) reflect.Value { +func structFields(value reflect.Value, tagName string) []reflect.StructField { + t := value.Type() + + var f []reflect.StructField + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + // we can't access the value of unexported fields + if field.PkgPath != "" { + continue + } + + // don't check if it's omitted + if tag := field.Tag.Get(tagName); tag == "-" { + continue + } + + f = append(f, field) + } + + return f +} + +func strctVal[T any](s T) reflect.Value { v := reflect.ValueOf(s) - // if pointer get the underlying element≤ + // if pointer get the underlying element for v.Kind() == reflect.Ptr { v = v.Elem() } @@ -506,7 +589,7 @@ func Name(s interface{}) string { // nested retrieves recursively all types for the given value and returns the // nested value. -func (s *Struct) nested(val reflect.Value) interface{} { +func (s *Struct[T]) nested(val reflect.Value) interface{} { var finalVal interface{} v := reflect.ValueOf(val.Interface()) diff --git a/structs_test.go b/structs_test.go index 073882e..962b55c 100644 --- a/structs_test.go +++ b/structs_test.go @@ -1451,3 +1451,121 @@ func TestMap_InterfaceTypeWithMapValue(t *testing.T) { _ = Map(a) } + +func TestAnonymousFieldStruct(t *testing.T) { + type A struct { + Name string `structs:"name"` + } + + a := struct { + Field int + A + }{ + Field: 100, + A: A{Name: "check"}, + } + + defer func() { + err := recover() + if err != nil { + t.Error("Converting Map with an interface{} type with map value should not panic") + } + }() + + result := Map(a) + + if val, exists := result["name"]; !exists || val != "check" { + t.Errorf("Value for anonymous field must exist") + } +} + +func TestStruct_SetValuesFromMap(t *testing.T) { + t.Parallel() + + type args struct { + values map[string]interface{} + } + + type Nested struct { + D int `json:"d"` + E string `json:"e"` + } + + type some struct { + A int `json:"tag"` + B string + C int + MapField map[string]string + Field []int + Now time.Time + StructField Nested `json:"struct_field"` + Nested + } + + tests := []struct { + name string + val *Struct[*some] + args args + want *some + }{ + { + name: "simple_struct", + val: New( + &some{ + A: 100, + B: "value", + C: 5, + MapField: map[string]string{"key": "value"}, + Field: nil, + }, + ), + args: args{ + values: map[string]interface{}{ + "tag": int(110), + "B": "new_value", + "MapField": map[string]string{"new_key": "value"}, + "Field": []int{1, 2, 3}, + "struct_field": map[string]interface{}{ + "d": 100, + "e": "nested_string", + }, + "Now": time.Date(2024, 01, 10, 10, 11, 0, 0, time.UTC), + "d": 120, + "e": "nested", + }, + }, + want: &some{ + A: 110, + B: "new_value", + C: 5, + MapField: map[string]string{"new_key": "value"}, + Field: []int{1, 2, 3}, + StructField: Nested{ + D: 100, + E: "nested_string", + }, + Now: time.Date(2024, 01, 10, 10, 11, 0, 0, time.UTC), + Nested: Nested{ + D: 120, + E: "nested", + }, + }, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + t.Parallel() + + tt.val.TagName = "json" + + tt.val.SetValuesFromMap(tt.args.values) + + if !reflect.DeepEqual(tt.val.Raw(), tt.want) { + t.Errorf("not equal") + } + }, + ) + } +}