Add output format options for upcoming CLI client
This commit is contained in:
parent
1ef1f59402
commit
13a38298ec
5 changed files with 309 additions and 0 deletions
46
internal/format/console_writer.go
Normal file
46
internal/format/console_writer.go
Normal 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)
|
||||||
|
}
|
13
internal/format/json_writer.go
Normal file
13
internal/format/json_writer.go
Normal 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)
|
||||||
|
}
|
103
internal/format/table_writer.go
Normal file
103
internal/format/table_writer.go
Normal 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
|
||||||
|
}
|
134
internal/format/table_writer_test.go
Normal file
134
internal/format/table_writer_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
13
internal/format/yaml_writer.go
Normal file
13
internal/format/yaml_writer.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue