Skip to content

Commit

Permalink
Merge pull request olekukonko#182 from johejo/add_new_func_to_render_…
Browse files Browse the repository at this point in the history
…slice_of_struct

Add new func to render slice of struct
  • Loading branch information
mattn authored Mar 4, 2021
2 parents 3ae52b6 + 9a79970 commit 74c60be
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 3 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ module github.com/olekukonko/tablewriter

go 1.12

require github.com/mattn/go-runewidth v0.0.9
require github.com/mattn/go-runewidth v0.0.10
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
91 changes: 91 additions & 0 deletions table.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ package tablewriter

import (
"bytes"
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
)
Expand Down Expand Up @@ -302,6 +304,95 @@ func (t *Table) SetBorders(border Border) {
t.borders = border
}

// SetStructs sets header and rows from slice of struct.
// If something that is not a slice is passed, error will be returned.
// The tag specified by "tablewriter" for the struct becomes the header.
// If not specified or empty, the field name will be used.
// The field of the first element of the slice is used as the header.
// If the element implements fmt.Stringer, the result will be used.
// And the slice contains nil, it will be skipped without rendering.
func (t *Table) SetStructs(v interface{}) error {
if v == nil {
return errors.New("nil value")
}
vt := reflect.TypeOf(v)
vv := reflect.ValueOf(v)
switch vt.Kind() {
case reflect.Slice, reflect.Array:
if vv.Len() < 1 {
return errors.New("empty value")
}

// check first element to set header
first := vv.Index(0)
e := first.Type()
switch e.Kind() {
case reflect.Struct:
// OK
case reflect.Ptr:
if first.IsNil() {
return errors.New("the first element is nil")
}
e = first.Elem().Type()
if e.Kind() != reflect.Struct {
return fmt.Errorf("invalid kind %s", e.Kind())
}
default:
return fmt.Errorf("invalid kind %s", e.Kind())
}
n := e.NumField()
headers := make([]string, n)
for i := 0; i < n; i++ {
f := e.Field(i)
header := f.Tag.Get("tablewriter")
if header == "" {
header = f.Name
}
headers[i] = header
}
t.SetHeader(headers)

for i := 0; i < vv.Len(); i++ {
item := reflect.Indirect(vv.Index(i))
itemType := reflect.TypeOf(item)
switch itemType.Kind() {
case reflect.Struct:
// OK
default:
return fmt.Errorf("invalid item type %v", itemType.Kind())
}
if !item.IsValid() {
// skip rendering
continue
}
nf := item.NumField()
if n != nf {
return errors.New("invalid num of field")
}
rows := make([]string, nf)
for j := 0; j < nf; j++ {
f := reflect.Indirect(item.Field(j))
if f.Kind() == reflect.Ptr {
f = f.Elem()
}
if f.IsValid() {
if s, ok := f.Interface().(fmt.Stringer); ok {
rows[j] = s.String()
continue
}
rows[j] = fmt.Sprint(f)
} else {
rows[j] = "nil"
}
}
t.Append(rows)
}
default:
return fmt.Errorf("invalid type %T", v)
}
return nil
}

// Append row to table
func (t *Table) Append(row []string) {
rowSize := len(t.headers)
Expand Down
197 changes: 197 additions & 0 deletions table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
)

func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) {
t.Helper()
if !reflect.DeepEqual(got, want) {
buf := bytes.Buffer{}
buf.WriteString("got:\n[%v]\nwant:\n[%v]\n")
Expand Down Expand Up @@ -1180,3 +1181,199 @@ func TestKubeFormat(t *testing.T) {

checkEqual(t, buf.String(), want, "kube format rendering failed")
}

type testStringerType struct{}

func (t testStringerType) String() string { return "testStringerType" }

func TestStructs(t *testing.T) {
type testType struct {
A string
B int
C testStringerType
D bool `tablewriter:"DD"`
}
type testType2 struct {
A *string
B *int
C *testStringerType
D *bool `tablewriter:"DD"`
}
type testType3 struct {
A **string
B **int
C **testStringerType
D **bool `tablewriter:"DD"`
}
a := "a"
b := 1
c := testStringerType{}
d := true

ap := &a
bp := &b
cp := &c
dp := &d

tests := []struct {
name string
values interface{}
wantErr bool
want string
}{
{
name: "slice of struct",
values: []testType{
{A: "AAA", B: 11, D: true},
{A: "BBB", B: 22},
},
want: `
+-----+----+------------------+-------+
| A | B | C | DD |
+-----+----+------------------+-------+
| AAA | 11 | testStringerType | true |
| BBB | 22 | testStringerType | false |
+-----+----+------------------+-------+
`,
},
{
name: "slice of struct pointer",
values: []*testType{
{A: "AAA", B: 11, D: true},
{A: "BBB", B: 22},
},
want: `
+-----+----+------------------+-------+
| A | B | C | DD |
+-----+----+------------------+-------+
| AAA | 11 | testStringerType | true |
| BBB | 22 | testStringerType | false |
+-----+----+------------------+-------+
`,
},
{
name: "pointer field",
values: []*testType2{
{A: &a, B: &b, C: &c, D: &d},
},
want: `
+---+---+------------------+------+
| A | B | C | DD |
+---+---+------------------+------+
| a | 1 | testStringerType | true |
+---+---+------------------+------+
`,
},
{
name: "nil pointer field",
values: []*testType2{
{A: nil, B: nil, C: nil, D: nil},
},
want: `
+-----+-----+-----+-----+
| A | B | C | DD |
+-----+-----+-----+-----+
| nil | nil | nil | nil |
+-----+-----+-----+-----+
`,
},
{
name: "typed nil pointer field",
values: []*testType2{
{A: (*string)(nil), B: (*int)(nil), C: (*testStringerType)(nil), D: (*bool)(nil)},
},
want: `
+-----+-----+-----+-----+
| A | B | C | DD |
+-----+-----+-----+-----+
| nil | nil | nil | nil |
+-----+-----+-----+-----+
`,
},
{
name: "pointer of pointer field",
values: []*testType3{
{A: &ap, B: &bp, C: &cp, D: &dp},
},
want: `
+---+---+------------------+------+
| A | B | C | DD |
+---+---+------------------+------+
| a | 1 | testStringerType | true |
+---+---+------------------+------+
`,
},
{
name: "invalid input",
values: interface{}(1),
wantErr: true,
},
{
name: "invalid input",
values: testType{},
wantErr: true,
},
{
name: "invalid input",
values: &testType{},
wantErr: true,
},
{
name: "nil value",
values: nil,
wantErr: true,
},
{
name: "the first element is nil",
values: []*testType{nil, nil},
wantErr: true,
},
{
name: "empty slice",
values: []testType{},
wantErr: true,
},
{
name: "mixed slice", // TODO: Should we support this case?
values: []interface{}{
testType{A: "a", B: 2, C: c, D: false},
testType2{A: &a, B: &b, C: &c, D: &d},
testType3{A: &ap, B: &bp, C: &cp, D: &dp},
},
wantErr: true,
},
{
name: "skip nil element",
values: []*testType{
{A: "a", B: 1, D: true},
nil,
nil,
{A: "A", B: 3, D: false},
},
want: `
+---+---+------------------+-------+
| A | B | C | DD |
+---+---+------------------+-------+
| a | 1 | testStringerType | true |
| A | 3 | testStringerType | false |
+---+---+------------------+-------+
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
table := NewWriter(&buf)
err := table.SetStructs(tt.values)
if tt.wantErr != (err != nil) {
t.Fatal(tt.wantErr, err)
}
if tt.wantErr {
t.Log(err)
return
}
table.Render()
checkEqual(t, buf.String(), strings.TrimPrefix(tt.want, "\n"))
})
}
}

0 comments on commit 74c60be

Please sign in to comment.