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= diff --git a/table.go b/table.go index f913149..5cf07c4 100644 --- a/table.go +++ b/table.go @@ -10,8 +10,10 @@ package tablewriter import ( "bytes" + "errors" "fmt" "io" + "reflect" "regexp" "strings" ) @@ -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) diff --git a/table_test.go b/table_test.go index 14bbede..3a746fb 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,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")) + }) + } +}