Skip to content

Commit

Permalink
added StructFieldNaming.Columns and recursion into anonymous fields
Browse files Browse the repository at this point in the history
  • Loading branch information
ungerik committed Oct 17, 2024
1 parent b26afec commit b1afe93
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 10 deletions.
85 changes: 75 additions & 10 deletions structfieldnaming.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type StructFieldNaming struct {
}

// String implements the fmt.Stringer interface for StructFieldNaming.
//
// Valid to call with nil receiver.
func (n *StructFieldNaming) String() string {
if n == nil {
return `StructFieldNaming{Tag: "", Ignore: ""}`
Expand All @@ -39,12 +41,17 @@ func (n *StructFieldNaming) String() string {
}

// StructFieldColumn returns the column title for a struct field.
func (n *StructFieldNaming) StructFieldColumn(structField reflect.StructField) string {
//
// Valid to call with nil receiver.
func (n *StructFieldNaming) StructFieldColumn(field reflect.StructField) string {
if !field.IsExported() || field.Anonymous {
return ""
}
if n == nil {
return structField.Name
return field.Name
}
if n.Tag != "" {
if tag, ok := structField.Tag.Lookup(n.Tag); ok {
if tag, ok := field.Tag.Lookup(n.Tag); ok {
if i := strings.IndexByte(tag, ','); i != -1 {
tag = tag[:i]
}
Expand All @@ -54,21 +61,79 @@ func (n *StructFieldNaming) StructFieldColumn(structField reflect.StructField) s
}
}
if n.Untagged == nil {
return structField.Name
return field.Name
}
return n.Untagged(structField.Name)
return n.Untagged(field.Name)
}

func (n *StructFieldNaming) ColumnStructFieldValue(strct reflect.Value, column string) reflect.Value {
strctType := strct.Type()
for i := 0; i < strctType.NumField(); i++ {
if n.StructFieldColumn(strctType.Field(i)) == column {
return strct.Field(i)
func (n *StructFieldNaming) IsIgnored(column string) bool {
return column == "" || (n != nil && column == n.Ignore)
}

// ColumnStructFieldValue returns the reflect.Value of the struct field
// that is mapped to the column title.
//
// Valid to call with nil receiver.
func (n *StructFieldNaming) ColumnStructFieldValue(structVal reflect.Value, column string) reflect.Value {
if n.IsIgnored(column) {
return reflect.Value{}
}
if structVal.Kind() == reflect.Pointer {
structVal = structVal.Elem()
}
structType := structVal.Type()
if structType.Kind() != reflect.Struct {
panic("expected struct or pointer to struct instead of " + structVal.Type().String())
}
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
if field.Anonymous {
// Recurse into anonymous embedded structs
if v := n.ColumnStructFieldValue(structVal.Field(i), column); v.IsValid() {
return v
}
continue
}
if n.StructFieldColumn(field) == column {
return structVal.Field(i)
}
}
return reflect.Value{}
}

// Columns returns the column titles for a struct
// or a pointer to a struct.
//
// It panics for non struct or struct pointer types.
//
// Valid to call with nil receiver.
func (n *StructFieldNaming) Columns(strct any) []string {
return n.columns(reflect.TypeOf(strct))
}

func (n *StructFieldNaming) columns(t reflect.Type) []string {
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
panic("expected struct or pointer to struct instead of " + t.String())
}
columns := make([]string, 0, t.NumField())
for i := range t.NumField() {
field := t.Field(i)
if field.Anonymous {
// Recurse into anonymous embedded structs
columns = append(columns, n.columns(field.Type)...)
continue
}
column := n.StructFieldColumn(field)
if !n.IsIgnored(column) {
columns = append(columns, column)
}
}
return columns
}

// NewView returns a View for a table made up of
// a slice or array of structs.
// NewView implements the Viewer interface for StructFieldNaming.
Expand Down
104 changes: 104 additions & 0 deletions structfieldnaming_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package retable

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestStructFieldNaming_Columns(t *testing.T) {
type StructWithFloat struct {
Float float64 `col:"float"`
}
tests := []struct {
name string
naming *StructFieldNaming
strct any
want []string
}{
{
name: "empty struct, nil naming",
naming: nil,
strct: struct{}{},
want: []string{},
},
{
name: "exported names, nil naming",
naming: nil,
strct: struct {
Int int
Bool bool
}{},
want: []string{"Int", "Bool"},
},
{
name: "exported and private names, nil naming",
naming: nil,
strct: struct {
Int int
Bool bool
hidden string
}{},
want: []string{"Int", "Bool"},
},
{
name: "mixed, nil naming",
naming: nil,
strct: struct {
Int int
StructWithFloat
Struct struct {
Sub bool
}
hidden string
}{},
want: []string{"Int", "Float", "Struct"},
},

{
name: "empty struct, DefaultStructFieldNaming",
naming: &DefaultStructFieldNaming,
strct: struct{}{},
want: []string{},
},
{
name: "exported names, DefaultStructFieldNaming",
naming: &DefaultStructFieldNaming,
strct: struct {
Int int
Bool bool `col:"boolean"`
}{},
want: []string{"Int", "boolean"},
},
{
name: "exported and private names, DefaultStructFieldNaming",
naming: &DefaultStructFieldNaming,
strct: struct {
Int int `col:"Integer"`
Bool bool `col:"-"`
hidden string
HelloWorld string
}{},
want: []string{"Integer", "Hello World"},
},
{
name: "mixed, DefaultStructFieldNaming",
naming: &DefaultStructFieldNaming,
strct: struct {
hidden string `col:"-"`
Int int
StructWithFloat
Struct struct {
Sub bool
}
}{},
want: []string{"Int", "float", "Struct"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.naming.Columns(tt.strct)
require.Equal(t, tt.want, got, "StructFieldNaming.Columns()")
})
}
}

0 comments on commit b1afe93

Please sign in to comment.