From 47662abae16d9244776bf57cce4e3a262517af1c Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Thu, 11 Feb 2021 17:06:12 +0900 Subject: [PATCH 1/5] Add SetStructs 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. --- table.go | 65 +++++++++++++++++++++++++++++++++++++++++ table_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) diff --git a/table.go b/table.go index f913149..73a18fe 100644 --- a/table.go +++ b/table.go @@ -10,8 +10,10 @@ package tablewriter import ( "bytes" + "errors" "fmt" "io" + "reflect" "regexp" "strings" ) @@ -302,6 +304,69 @@ 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. +func (t *Table) SetStructs(v interface{}) error { + 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") + } + first := vv.Index(0) + e := first.Type() + switch e.Kind() { + case reflect.Struct: + // OK + case reflect.Ptr: + 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()) + } + nf := item.NumField() + rows := make([]string, nf) + for j := 0; j < nf; j++ { + f := item.Field(j) + if s, ok := f.Interface().(fmt.Stringer); ok { + rows[j] = s.String() + continue + } + rows[j] = fmt.Sprint(f) + } + 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) diff --git a/table_test.go b/table_test.go index 14bbede..13ff704 100644 --- a/table_test.go +++ b/table_test.go @@ -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") @@ -1180,3 +1181,82 @@ 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"` + } + 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: "invalid input", + values: interface{}(1), + wantErr: true, + }, + { + name: "invalid input", + values: testType{}, + wantErr: true, + }, + { + name: "invalid input", + values: &testType{}, + wantErr: true, + }, + } + 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(), tt.want) + }) + } +} From 9627d02c9655319c8e1e36b8ef7f18d15c87d974 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Thu, 11 Feb 2021 17:06:51 +0900 Subject: [PATCH 2/5] Update mattn/go-runewidth to v0.0.10 --- go.mod | 2 +- go.sum | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 484ab01..9d442ae 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 4a94bf5..b8b450d 100644 --- a/go.sum +++ b/go.sum @@ -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= From c35d45cff8658f123045150d5d4b278bfbe1a7b7 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 3 Mar 2021 23:56:28 +0900 Subject: [PATCH 3/5] Support pointer field --- table.go | 18 +++++++++-- table_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 6 deletions(-) diff --git a/table.go b/table.go index 73a18fe..e40f1cf 100644 --- a/table.go +++ b/table.go @@ -308,7 +308,11 @@ func (t *Table) SetBorders(border Border) { // 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. +// If field implements fmt.Stringer, the result will be used. 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() { @@ -352,9 +356,17 @@ func (t *Table) SetStructs(v interface{}) error { nf := item.NumField() rows := make([]string, nf) for j := 0; j < nf; j++ { - f := item.Field(j) - if s, ok := f.Interface().(fmt.Stringer); ok { - rows[j] = s.String() + 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 + } + } else { + rows[j] = "nil" continue } rows[j] = fmt.Sprint(f) diff --git a/table_test.go b/table_test.go index 13ff704..3290ec6 100644 --- a/table_test.go +++ b/table_test.go @@ -1193,6 +1193,28 @@ func TestStructs(t *testing.T) { 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{} @@ -1205,7 +1227,8 @@ func TestStructs(t *testing.T) { {A: "AAA", B: 11, D: true}, {A: "BBB", B: 22}, }, - want: `+-----+----+------------------+-------+ + want: ` ++-----+----+------------------+-------+ | A | B | C | DD | +-----+----+------------------+-------+ | AAA | 11 | testStringerType | true | @@ -1219,12 +1242,65 @@ func TestStructs(t *testing.T) { {A: "AAA", B: 11, D: true}, {A: "BBB", B: 22}, }, - want: `+-----+----+------------------+-------+ + 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 | ++---+---+------------------+------+ `, }, { @@ -1242,6 +1318,11 @@ func TestStructs(t *testing.T) { values: &testType{}, wantErr: true, }, + { + name: "nil value", + values: nil, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1256,7 +1337,7 @@ func TestStructs(t *testing.T) { return } table.Render() - checkEqual(t, buf.String(), tt.want) + checkEqual(t, buf.String(), strings.TrimPrefix(tt.want, "\n")) }) } } From ed9444740084928d0f8c01264eddf13f90b47a8b Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Thu, 4 Mar 2021 00:45:59 +0900 Subject: [PATCH 4/5] Support more edge cases --- table.go | 17 ++++++++++++++++- table_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/table.go b/table.go index e40f1cf..9867b0f 100644 --- a/table.go +++ b/table.go @@ -308,7 +308,9 @@ func (t *Table) SetBorders(border Border) { // 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. -// If field implements fmt.Stringer, the result 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") @@ -320,12 +322,17 @@ func (t *Table) SetStructs(v interface{}) error { 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()) @@ -344,6 +351,7 @@ func (t *Table) SetStructs(v interface{}) error { headers[i] = header } t.SetHeader(headers) + for i := 0; i < vv.Len(); i++ { item := reflect.Indirect(vv.Index(i)) itemType := reflect.TypeOf(item) @@ -353,7 +361,14 @@ func (t *Table) SetStructs(v interface{}) error { 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)) diff --git a/table_test.go b/table_test.go index 3290ec6..3a746fb 100644 --- a/table_test.go +++ b/table_test.go @@ -1323,6 +1323,42 @@ func TestStructs(t *testing.T) { 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) { From 9a79970c0651e5994cb519839e9657823337a6ba Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Thu, 4 Mar 2021 00:50:58 +0900 Subject: [PATCH 5/5] Cleanup --- table.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/table.go b/table.go index 9867b0f..5cf07c4 100644 --- a/table.go +++ b/table.go @@ -380,11 +380,10 @@ func (t *Table) SetStructs(v interface{}) error { rows[j] = s.String() continue } + rows[j] = fmt.Sprint(f) } else { rows[j] = "nil" - continue } - rows[j] = fmt.Sprint(f) } t.Append(rows) }