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