diff --git a/internal/format/console_writer.go b/internal/format/console_writer.go new file mode 100644 index 0000000..0a87d12 --- /dev/null +++ b/internal/format/console_writer.go @@ -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) +} diff --git a/internal/format/json_writer.go b/internal/format/json_writer.go new file mode 100644 index 0000000..fe0922a --- /dev/null +++ b/internal/format/json_writer.go @@ -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) +} diff --git a/internal/format/table_writer.go b/internal/format/table_writer.go new file mode 100644 index 0000000..be5dbfd --- /dev/null +++ b/internal/format/table_writer.go @@ -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 "" + 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 +} diff --git a/internal/format/table_writer_test.go b/internal/format/table_writer_test.go new file mode 100644 index 0000000..2c35d40 --- /dev/null +++ b/internal/format/table_writer_test.go @@ -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) + } + }) + } +} diff --git a/internal/format/yaml_writer.go b/internal/format/yaml_writer.go new file mode 100644 index 0000000..a41d460 --- /dev/null +++ b/internal/format/yaml_writer.go @@ -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) +}