Add output format options for upcoming CLI client

This commit is contained in:
Peter 2020-05-03 10:16:07 +02:00 committed by baez90
parent 1ef1f59402
commit 13a38298ec
5 changed files with 309 additions and 0 deletions

View file

@ -0,0 +1,46 @@
package format
import (
"encoding/json"
"github.com/olekukonko/tablewriter"
"gopkg.in/yaml.v2"
"io"
"strings"
)
type consoleWriterFactory func(io.Writer) ConsoleWriter
var (
writers = map[string]consoleWriterFactory{
"table": func(writer io.Writer) ConsoleWriter {
tw := tablewriter.NewWriter(writer)
tw.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
tw.SetCenterSeparator("|")
return &tblWriter{
tableWriter: tw,
}
},
"json": func(writer io.Writer) ConsoleWriter {
return &jsonWriter{
encoder: json.NewEncoder(writer),
}
},
"yaml": func(writer io.Writer) ConsoleWriter {
return &yamlWriter{
encoder: yaml.NewEncoder(writer),
}
},
}
)
func Writer(format string, writer io.Writer) ConsoleWriter {
if cw, ok := writers[strings.ToLower(format)]; ok {
return cw(writer)
}
return writers["table"](writer)
}
type ConsoleWriter interface {
Write(in interface{}) (err error)
}

View file

@ -0,0 +1,13 @@
package format
import (
"encoding/json"
)
type jsonWriter struct {
encoder *json.Encoder
}
func (j *jsonWriter) Write(in interface{}) error {
return j.encoder.Encode(in)
}

View file

@ -0,0 +1,103 @@
package format
import (
"errors"
"fmt"
"github.com/olekukonko/tablewriter"
"reflect"
"strconv"
)
type tblWriter struct {
tableWriter *tablewriter.Table
}
func (t *tblWriter) Write(in interface{}) (err error) {
v := reflect.ValueOf(in)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
var vt reflect.Type
var numberOfFields int
data := make([][]string, 0)
switch v.Kind() {
case reflect.Interface:
return errors.New("interface{} is not supported")
case reflect.Slice, reflect.Array:
length := v.Len()
if length < 1 {
return
}
vt = v.Index(0).Type()
if vt.Kind() == reflect.Ptr {
vt = vt.Elem()
}
if vt.Kind() != reflect.Struct {
return fmt.Errorf("element type of array %v is not supported", vt.Kind())
}
numberOfFields = vt.NumField()
for i := 0; i < length; i++ {
data = append(data, t.getData(v.Index(i), numberOfFields))
}
case reflect.Struct:
vt = v.Type()
numberOfFields = vt.NumField()
data = append(data, t.getData(v, numberOfFields))
}
t.tableWriter.SetHeader(headersForType(vt, numberOfFields))
t.tableWriter.AppendBulk(data)
t.tableWriter.Render()
t.tableWriter.ClearRows()
return
}
func (t *tblWriter) getData(val reflect.Value, numberOfFields int) (data []string) {
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
for i := 0; i < numberOfFields; i++ {
data = append(data, value(val.Field(i)))
}
return
}
func value(val reflect.Value) string {
switch val.Kind() {
case reflect.Ptr:
return value(val.Elem())
case reflect.Struct, reflect.Interface:
return "<not supported>"
case reflect.Bool:
return strconv.FormatBool(val.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return strconv.FormatInt(val.Int(), 10)
case reflect.Float32, reflect.Float64:
return strconv.FormatFloat(val.Float(), 'f', 6, 64)
default:
return val.String()
}
}
func headersForType(t reflect.Type, numberOfFields int) (headers []string) {
for i := 0; i < numberOfFields; i++ {
field := t.Field(i)
if tableTag, ok := field.Tag.Lookup("table"); ok {
headers = append(headers, tableTag)
} else {
headers = append(headers, field.Name)
}
}
return
}

View file

@ -0,0 +1,134 @@
package format
import (
"strings"
"testing"
)
func Test_tblWriter_Write(t *testing.T) {
type s1 struct {
Name string
Age int
}
type s2 struct {
Name string `table:"Full name"`
Age int `table:"Age in years"`
}
type args struct {
in interface{}
}
tests := []struct {
name string
args args
wantErr bool
wantResult string
}{
{
name: "Test write table without errors",
args: args{
in: s1{
Name: "Ted Tester",
Age: 28,
},
},
wantErr: false,
wantResult: `
| NAME | AGE |
|------------|-----|
| Ted Tester | 28 |
`,
},
{
name: "Test write table without errors with pointer value",
args: args{
in: &s1{
Name: "Ted Tester",
Age: 28,
},
},
wantErr: false,
wantResult: `
| NAME | AGE |
|------------|-----|
| Ted Tester | 28 |
`,
},
{
name: "Test write table without errors with multiple rows",
args: args{
in: []s1{
{
Name: "Ted Tester",
Age: 28,
},
{
Name: "Heinz",
Age: 33,
},
},
},
wantErr: false,
wantResult: `
| NAME | AGE |
|------------|-----|
| Ted Tester | 28 |
| Heinz | 33 |
`,
},
{
name: "Test write table without errors with multiple pointer rows",
args: args{
in: []*s1{
{
Name: "Ted Tester",
Age: 28,
},
{
Name: "Heinz",
Age: 33,
},
},
},
wantErr: false,
wantResult: `
| NAME | AGE |
|------------|-----|
| Ted Tester | 28 |
| Heinz | 33 |
`,
},
{
name: "Test write table without errors and with custom headers",
args: args{
in: s2{
Name: "Ted Tester",
Age: 28,
},
},
wantErr: false,
wantResult: `
| FULL NAME | AGE IN YEARS |
|------------|--------------|
| Ted Tester | 28 |
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bldr := &strings.Builder{}
// hack to be able to format expected strings pretty
bldr.WriteRune('\n')
tw := Writer("table", bldr)
if err := tw.Write(tt.args.in); (err != nil) != tt.wantErr {
t.Errorf("Write() error = %v, wantErr %v", err, tt.wantErr)
return
}
if bldr.String() != tt.wantResult {
t.Errorf("Write() got = %s, want %s", bldr.String(), tt.wantResult)
}
})
}
}

View file

@ -0,0 +1,13 @@
package format
import (
"gopkg.in/yaml.v2"
)
type yamlWriter struct {
encoder *yaml.Encoder
}
func (y *yamlWriter) Write(in interface{}) (err error) {
return y.encoder.Encode(in)
}