diff --git a/structfieldnaming.go b/structfieldnaming.go index 10cc72e..3a9f8fa 100644 --- a/structfieldnaming.go +++ b/structfieldnaming.go @@ -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: ""}` @@ -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] } @@ -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. diff --git a/structfieldnaming_test.go b/structfieldnaming_test.go new file mode 100644 index 0000000..8b2fea1 --- /dev/null +++ b/structfieldnaming_test.go @@ -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()") + }) + } +}